diff --git a/.changeset/lovely-ravens-crash.md b/.changeset/lovely-ravens-crash.md new file mode 100644 index 0000000000..be0c4130ad --- /dev/null +++ b/.changeset/lovely-ravens-crash.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix(types): export CompileResult and Warning diff --git a/.changeset/mighty-shoes-nail.md b/.changeset/mighty-shoes-nail.md new file mode 100644 index 0000000000..0a758d5f6b --- /dev/null +++ b/.changeset/mighty-shoes-nail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure element dir properties persist with text changes diff --git a/.changeset/olive-cobras-wonder.md b/.changeset/olive-cobras-wonder.md new file mode 100644 index 0000000000..331d61a404 --- /dev/null +++ b/.changeset/olive-cobras-wonder.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: disallow accessing internal Svelte props diff --git a/.changeset/popular-feet-rule.md b/.changeset/popular-feet-rule.md new file mode 100644 index 0000000000..fee598af45 --- /dev/null +++ b/.changeset/popular-feet-rule.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make media bindings more robust diff --git a/.changeset/six-gorillas-obey.md b/.changeset/six-gorillas-obey.md new file mode 100644 index 0000000000..258505a381 --- /dev/null +++ b/.changeset/six-gorillas-obey.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: allow `let props = $props()` and optimize prop read access diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 57fa1ed700..d31c04b99c 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -78,6 +78,10 @@ > Cannot use `$props()` more than once +## props_illegal_name + +> Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals) + ## props_invalid_identifier > `$props()` can only be used with an object destructuring pattern diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index d6d3d51d7c..1a60f99000 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -276,6 +276,15 @@ export function props_duplicate(node) { e(node, "props_duplicate", "Cannot use `$props()` more than once"); } +/** + * Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals) + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function props_illegal_name(node) { + e(node, "props_illegal_name", "Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)"); +} + /** * `$props()` can only be used with an object destructuring pattern * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 6986408f04..06cb49b1bc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -31,6 +31,7 @@ import { hash } from './utils.js'; import { warn_unused } from './css/css-warn.js'; import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js'; +import { equal } from '../../utils/assert.js'; /** * @param {import('#compiler').Script | null} script @@ -969,34 +970,42 @@ const runes_scope_tweaker = { if (rune === '$props') { state.analysis.needs_props = true; - for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) { - if (property.type !== 'Property') continue; - - const name = - property.value.type === 'AssignmentPattern' - ? /** @type {import('estree').Identifier} */ (property.value.left).name - : /** @type {import('estree').Identifier} */ (property.value).name; - const alias = - property.key.type === 'Identifier' - ? property.key.name - : String(/** @type {import('estree').Literal} */ (property.key).value); - let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; - - const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name)); - binding.prop_alias = alias; - - // rewire initial from $props() to the actual initial value, stripping $bindable() if necessary - if ( - initial?.type === 'CallExpression' && - initial.callee.type === 'Identifier' && - initial.callee.name === '$bindable' - ) { - binding.initial = /** @type {import('estree').Expression | null} */ ( - initial.arguments[0] ?? null - ); - binding.kind = 'bindable_prop'; - } else { - binding.initial = initial; + if (node.id.type === 'Identifier') { + const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name)); + binding.initial = null; // else would be $props() + binding.kind = 'rest_prop'; + } else { + equal(node.id.type, 'ObjectPattern'); + + for (const property of node.id.properties) { + if (property.type !== 'Property') continue; + + const name = + property.value.type === 'AssignmentPattern' + ? /** @type {import('estree').Identifier} */ (property.value.left).name + : /** @type {import('estree').Identifier} */ (property.value).name; + const alias = + property.key.type === 'Identifier' + ? property.key.name + : String(/** @type {import('estree').Literal} */ (property.key).value); + let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; + + const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name)); + binding.prop_alias = alias; + + // rewire initial from $props() to the actual initial value, stripping $bindable() if necessary + if ( + initial?.type === 'CallExpression' && + initial.callee.type === 'Identifier' && + initial.callee.name === '$bindable' + ) { + binding.initial = /** @type {import('estree').Expression | null} */ ( + initial.arguments[0] ?? null + ); + binding.kind = 'bindable_prop'; + } else { + binding.initial = initial; + } } } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index d0258a52dc..b2097e60ce 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -341,6 +341,14 @@ function validate_block_not_empty(node, context) { * @type {import('zimmerframe').Visitors} */ const validation = { + MemberExpression(node, context) { + if (node.object.type === 'Identifier' && node.property.type === 'Identifier') { + const binding = context.state.scope.get(node.object.name); + if (binding?.kind === 'rest_prop' && node.property.name.startsWith('$$')) { + e.props_illegal_name(node.property); + } + } + }, AssignmentExpression(node, context) { validate_assignment(node, node.left, context.state); }, @@ -1244,7 +1252,7 @@ export const validation_runes = merge(validation, a11y_validators, { e.rune_invalid_arguments(node, rune); } - if (node.id.type !== 'ObjectPattern') { + if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') { e.props_invalid_identifier(node); } @@ -1252,17 +1260,23 @@ export const validation_runes = merge(validation, a11y_validators, { e.props_invalid_placement(node); } - for (const property of node.id.properties) { - if (property.type === 'Property') { - if (property.computed) { - e.props_invalid_pattern(property); - } + if (node.id.type === 'ObjectPattern') { + for (const property of node.id.properties) { + if (property.type === 'Property') { + if (property.computed) { + e.props_invalid_pattern(property); + } + + if (property.key.type === 'Identifier' && property.key.name.startsWith('$$')) { + e.props_illegal_name(property); + } - const value = - property.value.type === 'AssignmentPattern' ? property.value.left : property.value; + const value = + property.value.type === 'AssignmentPattern' ? property.value.left : property.value; - if (value.type !== 'Identifier') { - e.props_invalid_pattern(property); + if (value.type !== 'Identifier') { + e.props_invalid_pattern(property); + } } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js index 37dd02855f..a640fb01b0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js @@ -9,6 +9,27 @@ export const global_visitors = { if (node.name === '$$props') { return b.id('$$sanitized_props'); } + + // Optimize prop access: If it's a member read access, we can use the $$props object directly + const binding = state.scope.get(node.name); + if ( + state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write + binding !== null && + node !== binding.node && + binding.kind === 'rest_prop' + ) { + const parent = path.at(-1); + const grand_parent = path.at(-2); + if ( + parent?.type === 'MemberExpression' && + !parent.computed && + grand_parent?.type !== 'AssignmentExpression' && + grand_parent?.type !== 'UpdateExpression' + ) { + return b.id('$$props'); + } + } + return serialize_get_binding(node, state); } }, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index 91934738b0..4588f71d47 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -238,8 +238,6 @@ export const javascript_visitors_runes = { } if (rune === '$props') { - assert.equal(declarator.id.type, 'ObjectPattern'); - /** @type {string[]} */ const seen = ['$$slots', '$$events', '$$legacy']; @@ -247,44 +245,58 @@ export const javascript_visitors_runes = { seen.push('$$host'); } - for (const property of declarator.id.properties) { - if (property.type === 'Property') { - const key = /** @type {import('estree').Identifier | import('estree').Literal} */ ( - property.key - ); - const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value); - - seen.push(name); - - let id = - property.value.type === 'AssignmentPattern' ? property.value.left : property.value; - assert.equal(id.type, 'Identifier'); - const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name)); - let initial = - binding.initial && - /** @type {import('estree').Expression} */ (visit(binding.initial)); - // We're adding proxy here on demand and not within the prop runtime function so that - // people not using proxied state anywhere in their code don't have to pay the additional bundle size cost - if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) { - initial = b.call('$.proxy', initial); - } + if (declarator.id.type === 'Identifier') { + /** @type {import('estree').Expression[]} */ + const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; - if (is_prop_source(binding, state)) { - declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial))); - } - } else { - // RestElement - /** @type {import('estree').Expression[]} */ - const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; - - if (state.options.dev) { - // include rest name, so we can provide informative error messages - args.push( - b.literal(/** @type {import('estree').Identifier} */ (property.argument).name) + if (state.options.dev) { + // include rest name, so we can provide informative error messages + args.push(b.literal(declarator.id.name)); + } + + declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args))); + } else { + assert.equal(declarator.id.type, 'ObjectPattern'); + + for (const property of declarator.id.properties) { + if (property.type === 'Property') { + const key = /** @type {import('estree').Identifier | import('estree').Literal} */ ( + property.key ); + const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value); + + seen.push(name); + + let id = + property.value.type === 'AssignmentPattern' ? property.value.left : property.value; + assert.equal(id.type, 'Identifier'); + const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name)); + let initial = + binding.initial && + /** @type {import('estree').Expression} */ (visit(binding.initial)); + // We're adding proxy here on demand and not within the prop runtime function so that + // people not using proxied state anywhere in their code don't have to pay the additional bundle size cost + if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) { + initial = b.call('$.proxy', initial); + } + + if (is_prop_source(binding, state)) { + declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial))); + } + } else { + // RestElement + /** @type {import('estree').Expression[]} */ + const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; + + if (state.options.dev) { + // include rest name, so we can provide informative error messages + args.push( + b.literal(/** @type {import('estree').Identifier} */ (property.argument).name) + ); + } + + declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args))); } - - declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args))); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index a4087fb9b3..4e8fcb0faa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1973,6 +1973,7 @@ export const template_visitors = { let has_content_editable_binding = false; let img_might_be_lazy = false; let might_need_event_replaying = false; + let has_direction_attribute = false; if (is_custom_element) { // cloneNode is faster, but it does not instantiate the underlying class of the @@ -1988,6 +1989,9 @@ export const template_visitors = { if (node.name === 'img' && attribute.name === 'loading') { img_might_be_lazy = true; } + if (attribute.name === 'dir') { + has_direction_attribute = true; + } if ( (attribute.name === 'value' || attribute.name === 'checked') && !is_text_attribute(attribute) @@ -2188,6 +2192,14 @@ export const template_visitors = { { ...context, state } ); + if (has_direction_attribute) { + // This fixes an issue with Chromium where updates to text content within an element + // does not update the direction when set to auto. If we just re-assign the dir, this fixes it. + context.state.update.push( + b.stmt(b.assignment('=', b.member(node_id, b.id('dir')), b.member(node_id, b.id('dir')))) + ); + } + if (child_locations.length > 0) { // @ts-expect-error location.push(child_locations); diff --git a/packages/svelte/src/compiler/public.d.ts b/packages/svelte/src/compiler/public.d.ts index cd91c97f47..53742324e9 100644 --- a/packages/svelte/src/compiler/public.d.ts +++ b/packages/svelte/src/compiler/public.d.ts @@ -5,4 +5,4 @@ export type { PreprocessorGroup, Processed } from './preprocess/public'; -export type { CompileOptions, ModuleCompileOptions } from './types/index'; +export type { CompileOptions, ModuleCompileOptions, CompileResult, Warning } from './types/index'; diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/media.js b/packages/svelte/src/internal/client/dom/elements/bindings/media.js index b7708b4f34..320e3856f2 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/media.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/media.js @@ -22,7 +22,8 @@ function time_ranges_to_array(ranges) { export function bind_current_time(media, get_value, update) { /** @type {number} */ var raf_id; - var updating = false; + /** @type {number} */ + var value; // Ideally, listening to timeupdate would be enough, but it fires too infrequently for the currentTime // binding, which is why we use a raf loop, too. We additionally still listen to timeupdate because @@ -34,22 +35,21 @@ export function bind_current_time(media, get_value, update) { raf_id = requestAnimationFrame(callback); } - updating = true; - update(media.currentTime); + var next_value = media.currentTime; + if (value !== next_value) { + update((value = next_value)); + } }; raf_id = requestAnimationFrame(callback); media.addEventListener('timeupdate', callback); render_effect(() => { - var value = get_value(); + var next_value = Number(get_value()); - // through isNaN we also allow number strings, which is more robust - if (!updating && !isNaN(/** @type {any} */ (value))) { - media.currentTime = /** @type {number} */ (value); + if (value !== next_value && !isNaN(/** @type {any} */ (next_value))) { + media.currentTime = value = next_value; } - - updating = false; }); teardown(() => cancelAnimationFrame(raf_id)); @@ -113,22 +113,21 @@ export function bind_ready_state(media, update) { * @param {(playback_rate: number) => void} update */ export function bind_playback_rate(media, get_value, update) { - var updating = false; - - // Needs to happen after the element is inserted into the dom, else playback will be set back to 1 by the browser. - // For hydration we could do it immediately but the additional code is not worth the lost microtask. + // Needs to happen after element is inserted into the dom (which is guaranteed by using effect), + // else playback will be set back to 1 by the browser effect(() => { - var value = get_value(); + var value = Number(get_value()); - // through isNaN we also allow number strings, which is more robust - if (!isNaN(/** @type {any} */ (value)) && value !== media.playbackRate) { - updating = true; - media.playbackRate = /** @type {number} */ (value); + if (value !== media.playbackRate && !isNaN(value)) { + media.playbackRate = value; } + }); + // Start listening to ratechange events after the element is inserted into the dom, + // else playback will be set to 1 by the browser + effect(() => { listen(media, ['ratechange'], () => { - if (!updating) update(media.playbackRate); - updating = false; + update(media.playbackRate); }); }); } @@ -200,9 +199,7 @@ export function bind_paused(media, get_value, update) { * @param {(volume: number) => void} update */ export function bind_volume(media, get_value, update) { - var updating = false; var callback = () => { - updating = true; update(media.volume); }; @@ -213,14 +210,11 @@ export function bind_volume(media, get_value, update) { listen(media, ['volumechange'], callback, false); render_effect(() => { - var value = get_value(); + var value = Number(get_value()); - // through isNaN we also allow number strings, which is more robust - if (!updating && !isNaN(/** @type {any} */ (value))) { - media.volume = /** @type {number} */ (value); + if (value !== media.volume && !isNaN(value)) { + media.volume = value; } - - updating = false; }); } @@ -230,10 +224,7 @@ export function bind_volume(media, get_value, update) { * @param {(muted: boolean) => void} update */ export function bind_muted(media, get_value, update) { - var updating = false; - var callback = () => { - updating = true; update(media.muted); }; @@ -244,9 +235,8 @@ export function bind_muted(media, get_value, update) { listen(media, ['volumechange'], callback, false); render_effect(() => { - var value = get_value(); + var value = !!get_value(); - if (!updating) media.muted = !!value; - updating = false; + if (media.muted !== value) media.muted = value; }); } diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 3232239a75..c6b73ee6b5 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -78,6 +78,7 @@ const rest_props_handler = { * @param {string} [name] * @returns {Record} */ +/*#__NO_SIDE_EFFECTS__*/ export function rest_props(props, exclude, name) { return new Proxy( DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude }, diff --git a/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/_config.js new file mode 100644 index 0000000000..b205e0de38 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'props_illegal_name', + message: + 'Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/main.svelte new file mode 100644 index 0000000000..dd581833fd --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/main.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/_config.js new file mode 100644 index 0000000000..b205e0de38 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'props_illegal_name', + message: + 'Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/main.svelte new file mode 100644 index 0000000000..3b287f829e --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/main.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/svelte/tests/runtime-browser/assert.js b/packages/svelte/tests/runtime-browser/assert.js index c2c265d90f..c30467279c 100644 --- a/packages/svelte/tests/runtime-browser/assert.js +++ b/packages/svelte/tests/runtime-browser/assert.js @@ -44,6 +44,7 @@ export function equal(a, b, message) { /** * @param {any} condition * @param {string} [message] + * @returns {asserts condition} */ export function ok(condition, message) { if (!condition) throw new Error(message || `Expected ${condition} to be truthy`); diff --git a/packages/svelte/tests/runtime-browser/samples/bind-muted/_config.js b/packages/svelte/tests/runtime-browser/samples/bind-muted/_config.js new file mode 100644 index 0000000000..cfc558a9c3 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-muted/_config.js @@ -0,0 +1,29 @@ +import { test, ok } from '../../assert'; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + const audio = target.querySelector('audio'); + const button = target.querySelector('button'); + ok(audio); + + assert.equal(audio.muted, false); + + audio.muted = true; + audio.dispatchEvent(new CustomEvent('volumechange')); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.muted, true, 'event'); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.muted, false, 'click 1'); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.muted, true, 'click 2'); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.muted, false, 'click 3'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/bind-muted/main.svelte b/packages/svelte/tests/runtime-browser/samples/bind-muted/main.svelte new file mode 100644 index 0000000000..ae03256e95 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-muted/main.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/_config.js b/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/_config.js new file mode 100644 index 0000000000..c2718e2850 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/_config.js @@ -0,0 +1,29 @@ +import { test, ok } from '../../assert'; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + const audio = target.querySelector('audio'); + const button = target.querySelector('button'); + ok(audio); + + assert.equal(audio.playbackRate, 0.5); + + audio.playbackRate = 1.0; + audio.dispatchEvent(new CustomEvent('ratechange')); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.playbackRate, 1.0); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.playbackRate, 2); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.playbackRate, 3); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.playbackRate, 4); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/main.svelte b/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/main.svelte new file mode 100644 index 0000000000..07c2080637 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/main.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/tests/runtime-browser/samples/bind-volume/_config.js b/packages/svelte/tests/runtime-browser/samples/bind-volume/_config.js new file mode 100644 index 0000000000..61f3db9692 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-volume/_config.js @@ -0,0 +1,29 @@ +import { test, ok } from '../../assert'; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + const audio = target.querySelector('audio'); + const button = target.querySelector('button'); + ok(audio); + + assert.equal(audio.volume, 0.1); + + audio.volume = 0.2; + audio.dispatchEvent(new CustomEvent('volumechange')); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.volume, 0.2); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.volume, 0.2 + 0.1); // JavaScript can't add floating point numbers correctly + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.volume, 0.2 + 0.1 + 0.1); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.volume, 0.2 + 0.1 + 0.1 + 0.1); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/bind-volume/main.svelte b/packages/svelte/tests/runtime-browser/samples/bind-volume/main.svelte new file mode 100644 index 0000000000..21755e0cba --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-volume/main.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js new file mode 100644 index 0000000000..2a10dbc1b1 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js @@ -0,0 +1,17 @@ +import "svelte/internal/disclose-version"; +import * as $ from "svelte/internal/client"; + +export default function Props_identifier($$anchor, $$props) { + $.push($$props, true); + + let props = $.rest_props($$props, ["$$slots", "$$events", "$$legacy"]); + + $$props.a; + props[a]; + $$props.a.b; + $$props.a.b = true; + props.a = true; + props[a] = true; + props; + $.pop(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js new file mode 100644 index 0000000000..362d773be1 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js @@ -0,0 +1,16 @@ +import * as $ from "svelte/internal/server"; + +export default function Props_identifier($$payload, $$props) { + $.push(); + + let props = $$props; + + props.a; + props[a]; + props.a.b; + props.a.b = true; + props.a = true; + props[a] = true; + props; + $.pop(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/index.svelte b/packages/svelte/tests/snapshot/samples/props-identifier/index.svelte new file mode 100644 index 0000000000..ebd9d09dca --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-identifier/index.svelte @@ -0,0 +1,10 @@ + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 36db59fe53..a325cab40a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1899,7 +1899,7 @@ declare module 'svelte/compiler' { attributes: Attribute[]; } - export { MarkupPreprocessor, Preprocessor, PreprocessorGroup, Processed, CompileOptions, ModuleCompileOptions, compile, compileModule, parse, walk, preprocess, CompileError, VERSION, migrate }; + export { MarkupPreprocessor, Preprocessor, PreprocessorGroup, Processed, CompileOptions, ModuleCompileOptions, CompileResult, Warning, compile, compileModule, parse, walk, preprocess, CompileError, VERSION, migrate }; } declare module 'svelte/easing' { diff --git a/playgrounds/demo/server.js b/playgrounds/demo/server.js index 0a545e7397..82a75e70e7 100644 --- a/playgrounds/demo/server.js +++ b/playgrounds/demo/server.js @@ -1,3 +1,4 @@ +// @ts-check import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -22,7 +23,10 @@ async function createServer() { app.use('*', async (req, res) => { if (req.originalUrl !== '/') { - res.sendFile(path.resolve('./dist' + req.originalUrl)); + res.writeHead(200, { + 'Content-Type': 'application/javascript' + }); + res.end(fs.createReadStream(path.resolve('./dist' + req.originalUrl))); return; } @@ -34,7 +38,7 @@ async function createServer() { .replace(``, appHtml) .replace(``, headHtml); - res.status(200).set({ 'Content-Type': 'text/html' }).end(html); + res.writeHead(200, { 'Content-Type': 'text/html' }).end(html); }); return { app, vite }; diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index 4a84e3a0f7..06b4f97834 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -337,7 +337,7 @@ In general, `$effect` is best considered something of an escape hatch — useful > For things that are more complicated than a simple expression like `count * 2`, you can also use [`$derived.by`](#$derived-by). -You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/#H4sIAAAAAAAACpVRy2rDMBD8lWXJwYE0dg-9KFYg31H3oNirIJBlYa1DjPG_F8l1XEop9LgzOzP7mFAbSwHF-4ROtYQCL97jAXn0sQh3skx4wNANfR2RMtS98XyuXMWWGLhjZUHCa1GcVix4cgwSdoEVU1bsn4wl_Y1I2kS6inekNdWcZXuQZ5giFDWpfwl5WYyT2fynbB1g1UWbTVbm2w6utOpKNq1TGucHhri6rLBX7kYVwtW4RtyVHUhOyXeGVj3klLxnyJP0i8lXNJUx6en-v6A48K85kTimpi0sYj-yAo-Wlh9FcL1LY4K3ahSgLT1OC3ZTXkBxfKN2uVC6T5LjAduuMdpQg4L7geaP-RNHPuClMQIAAA==)): +You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/#H4sIAAAAAAAACpVRQWrDMBD8ihA5ONDG7qEXxQ70HXUPir0KgrUsrHWIMf57pXWdlFIKPe6MZmZnNUtjEYJU77N0ugOp5Jv38knS5NMQroAEcQ79ODQJKUMzWE-n2tWEQIJ60igq8VIUxw0LHhxFbBdIE2TF_s4gmG8Ea5mM9A6MgYaybC-qk5gTlDT8fg15Xo3ZbPlTti2w6ZLNQ1bmjw6uRH0G5DqldX6MjWL1qpaDdheopThb16qrxhGqmX0X0elbNbP3InKWfjH5hvKYku7u_wtKC_-aw8Q9Jk0_UgJNCOvvJHC7SGuDRz0pYRBuxxW7aK9EcXiFbr0NX4bl8cO7vrXGQisVDSMsH8sniirsuSsCAAA=)): ```svelte