diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index a4000fb1115c..2a2d969475a1 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -167,8 +167,8 @@ const runes = { 'invalid-legacy-export': () => `Cannot use \`export let\` in runes mode — use $props instead`, /** @param {string} rune */ 'invalid-rune-usage': (rune) => `Cannot use ${rune} rune in non-runes mode`, - /** @param {string} rune */ - 'invalid-rune-export': (rune) => `Cannot export value created with ${rune}`, + 'invalid-state-export': () => `Cannot export state if it is reassigned`, + 'invalid-derived-export': () => `Cannot export derived state`, 'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`, 'invalid-props-pattern': () => `$props() assignment must not contain nested properties or computed keys`, diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 517827f7e345..1be80cc16986 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -183,7 +183,7 @@ function get_delegated_event(node, context) { // or any normal not reactive bindings that are mutated. binding.kind === 'normal' || // or any reactive imports (those are rewritten) (can only happen in legacy mode) - (binding.kind === 'state' && binding.declaration_kind === 'import')) && + binding.kind === 'legacy_reactive_import') && binding.mutated)) ) { return non_hoistable; @@ -221,20 +221,13 @@ export function analyze_module(ast, options) { merge(set_scope(scopes), validation_runes_js, runes_scope_js_tweaker) ); - // If we are in runes mode, then check for possible misuses of state runes - for (const [, scope] of scopes) { - for (const [name, binding] of scope.declarations) { - if (binding.kind === 'state' && !binding.mutated) { - warn(warnings, binding.node, [], 'state-not-mutated', name); - } - } - } - return { module: { ast, scope, scopes }, name: options.filename || 'module', warnings, - accessors: false + accessors: false, + runes: true, + immutable: true }; } @@ -315,6 +308,10 @@ export function analyze_component(root, options) { const component_name = get_component_name(options.filename ?? 'Component'); + const runes = + options.runes ?? + Array.from(module.scope.references).some(([name]) => Runes.includes(/** @type {any} */ (name))); + // TODO remove all the ?? stuff, we don't need it now that we're validating the config /** @type {import('../types.js').ComponentAnalysis} */ const analysis = { @@ -331,11 +328,8 @@ export function analyze_component(root, options) { component_name, get_css_hash: options.cssHash }), - runes: - options.runes ?? - Array.from(module.scope.references).some(([name]) => - Runes.includes(/** @type {any} */ (name)) - ), + runes, + immutable: runes || options.immutable, exports: [], uses_props: false, uses_rest_props: false, @@ -382,15 +376,6 @@ export function analyze_component(root, options) { merge(set_scope(scopes), validation_runes, runes_scope_tweaker, common_visitors) ); } - - // If we are in runes mode, then check for possible misuses of state runes - for (const [, scope] of instance.scopes) { - for (const [name, binding] of scope.declarations) { - if (binding.kind === 'state' && !binding.mutated) { - warn(warnings, binding.node, [], 'state-not-mutated', name); - } - } - } } else { instance.scope.declare(b.id('$$props'), 'prop', 'synthetic'); instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic'); @@ -553,7 +538,7 @@ const legacy_scope_tweaker = { (state.reactive_statement || state.ast_type === 'template') && parent.type === 'MemberExpression' ) { - binding.kind = 'state'; + binding.kind = 'legacy_reactive_import'; } } else if ( binding.mutated && diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 60aaf060f36a..232328289390 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -649,8 +649,14 @@ export const validation_legacy = merge(validation, a11y_validators, { */ function validate_export(node, scope, name) { const binding = scope.get(name); - if (binding && (binding.kind === 'derived' || binding.kind === 'state')) { - error(node, 'invalid-rune-export', `$${binding.kind}`); + if (!binding) return; + + if (binding.kind === 'derived') { + error(node, 'invalid-derived-export'); + } + + if (binding.kind === 'state' && binding.reassigned) { + error(node, 'invalid-state-export'); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 1afe2058b6d1..d69b70ec08e6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -159,7 +159,7 @@ export function client_component(source, analysis, options) { // Very very dirty way of making import statements reactive in legacy mode if needed if (!analysis.runes) { for (const [name, binding] of analysis.module.scope.declarations) { - if (binding.kind === 'state' && binding.declaration_kind === 'import') { + if (binding.kind === 'legacy_reactive_import') { instance.body.unshift( b.var('$$_import_' + name, b.call('$.reactive_import', b.thunk(b.id(name)))) ); @@ -175,7 +175,7 @@ export function client_component(source, analysis, options) { for (const [name, binding] of analysis.instance.scope.declarations) { if (binding.kind === 'legacy_reactive') { - legacy_reactive_declarations.push(b.const(name, b.call('$.source'))); + legacy_reactive_declarations.push(b.const(name, b.call('$.mutable_source'))); } if (binding.kind === 'store_sub') { if (store_setup.length === 0) { @@ -240,11 +240,12 @@ export function client_component(source, analysis, options) { const properties = analysis.exports.map(({ name, alias }) => { const binding = analysis.instance.scope.get(name); + const is_source = + binding?.kind === 'state' && (!state.analysis.immutable || binding.reassigned); + // TODO This is always a getter because the `renamed-instance-exports` test wants it that way. // Should we for code size reasons make it an init in runes mode and/or non-dev mode? - return b.get(alias ?? name, [ - b.return(binding?.kind === 'state' ? b.call('$.get', b.id(name)) : b.id(name)) - ]); + return b.get(alias ?? name, [b.return(is_source ? b.call('$.get', b.id(name)) : b.id(name))]); }); if (analysis.accessors) { @@ -261,14 +262,7 @@ export function client_component(source, analysis, options) { } const component_block = b.block([ - b.stmt( - b.call( - '$.push', - b.id('$$props'), - b.literal(analysis.runes), - ...(options.immutable ? [b.literal(true)] : []) - ) - ), + b.stmt(b.call('$.push', b.id('$$props'), b.literal(analysis.runes))), ...store_setup, ...legacy_reactive_declarations, ...group_binding_declarations, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 05633632c136..d6700add113d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,5 +1,5 @@ import * as b from '../../../utils/builders.js'; -import { extract_paths } from '../../../utils/ast.js'; +import { extract_paths, is_simple_expression } from '../../../utils/ast.js'; import { error } from '../../../errors.js'; /** @@ -67,19 +67,20 @@ export function serialize_get_binding(node, state) { if ( binding.kind === 'prop' && - !binding.mutated && + !(state.analysis.immutable ? binding.reassigned : binding.mutated) && !binding.initial && !state.analysis.accessors ) { return b.call(node); } - if (binding.kind === 'state' && binding.declaration_kind === 'import') { + if (binding.kind === 'legacy_reactive_import') { return b.call('$$_import_' + node.name); } if ( - binding.kind === 'state' || + (binding.kind === 'state' && + (!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) || binding.kind === 'derived' || binding.kind === 'prop' || binding.kind === 'rest_prop' || @@ -99,7 +100,7 @@ export function serialize_get_binding(node, state) { * @returns {import('estree').Expression} */ export function serialize_set_binding(node, context, fallback) { - const { state, visit, path } = context; + const { state, visit } = context; if ( node.left.type === 'ArrayPattern' || @@ -152,7 +153,11 @@ export function serialize_set_binding(node, context, fallback) { if (left.object.type === 'ThisExpression' && left.property.type === 'PrivateIdentifier') { if (context.state.private_state.has(left.property.name) && !state.in_constructor) { const value = get_assignment_value(node, context); - return b.call('$.set', left, value); + return b.call( + '$.set', + left, + context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value + ); } } // @ts-expect-error @@ -171,7 +176,7 @@ export function serialize_set_binding(node, context, fallback) { return binding.mutation(node, context); } - if (binding.kind === 'state' && binding.declaration_kind === 'import') { + if (binding.kind === 'legacy_reactive_import') { return b.call( '$$_import_' + binding.node.name, b.assignment( @@ -202,7 +207,11 @@ export function serialize_set_binding(node, context, fallback) { if (is_store) { return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value); } else { - return b.call('$.set', b.id(left_name), value); + return b.call( + '$.set', + b.id(left_name), + context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value + ); } } else { if (is_store) { @@ -216,7 +225,7 @@ export function serialize_set_binding(node, context, fallback) { ), b.call('$' + left_name) ); - } else { + } else if (!state.analysis.runes) { return b.call( '$.mutate', b.id(left_name), @@ -226,6 +235,12 @@ export function serialize_set_binding(node, context, fallback) { value ) ); + } else { + return b.assignment( + node.operator, + /** @type {import('estree').Pattern} */ (visit(node.left)), + /** @type {import('estree').Expression} */ (visit(node.right)) + ); } } }; @@ -335,24 +350,38 @@ export function get_props_method(binding, state, name, default_value) { /** @type {import('estree').Expression[]} */ const args = [b.id('$$props'), b.literal(name)]; + // Use $.prop_source in the following cases: + // - accessors/mutated: needs to be able to set the prop value from within + // - default value: we set the fallback value only initially, and it's not possible to know this timing in $.prop + const needs_source = + default_value || + state.analysis.accessors || + (state.analysis.immutable ? binding.reassigned : binding.mutated); + + if (needs_source) { + args.push(b.literal(state.analysis.immutable)); + } + if (default_value) { // To avoid eagerly evaluating the right-hand-side, we wrap it in a thunk if necessary - if (default_value.type !== 'Literal' && default_value.type !== 'Identifier') { - args.push(b.thunk(default_value)); - args.push(b.true); - } else { + if (is_simple_expression(default_value)) { args.push(default_value); - args.push(b.false); + } else { + if ( + default_value.type === 'CallExpression' && + default_value.callee.type === 'Identifier' && + default_value.arguments.length === 0 + ) { + args.push(default_value.callee); + } else { + args.push(b.thunk(default_value)); + } + + args.push(b.true); } } - return b.call( - // Use $.prop_source in the following cases: - // - accessors/mutated: needs to be able to set the prop value from within - // - default value: we set the fallback value only initially, and it's not possible to know this timing in $.prop - binding.mutated || binding.initial || state.analysis.accessors ? '$.prop_source' : '$.prop', - ...args - ); + return b.call(needs_source ? '$.prop_source' : '$.prop', ...args); } /** @@ -364,7 +393,7 @@ export function get_props_method(binding, state, name, default_value) { export function create_state_declarators(declarator, scope, value) { // in the simple `let count = $state(0)` case, we rewrite `$state` as `$.source` if (declarator.id.type === 'Identifier') { - return [b.declarator(declarator.id, b.call('$.source', value))]; + return [b.declarator(declarator.id, b.call('$.mutable_source', value))]; } const tmp = scope.generate('tmp'); @@ -374,7 +403,25 @@ export function create_state_declarators(declarator, scope, value) { ...paths.map((path) => { const value = path.expression?.(b.id(tmp)); const binding = scope.get(/** @type {import('estree').Identifier} */ (path.node).name); - return b.declarator(path.node, binding?.kind === 'state' ? b.call('$.source', value) : value); + return b.declarator( + path.node, + binding?.kind === 'state' ? b.call('$.mutable_source', value) : value + ); }) ]; } + +/** @param {import('estree').Expression} node */ +export function should_proxy(node) { + if ( + !node || + node.type === 'Literal' || + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionExpression' || + (node.type === 'Identifier' && node.name === 'undefined') + ) { + return false; + } + + return true; +} 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 5bb5216be669..01a6ec7df3ac 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 @@ -2,7 +2,7 @@ import { get_rune } from '../../../scope.js'; import { is_hoistable_function } from '../../utils.js'; import * as b from '../../../../utils/builders.js'; import * as assert from '../../../../utils/assert.js'; -import { create_state_declarators, get_props_method } from '../utils.js'; +import { create_state_declarators, get_props_method, should_proxy } from '../utils.js'; import { unwrap_ts_expression } from '../../../../utils/ast.js'; /** @type {import('../types.js').ComponentVisitors} */ @@ -84,7 +84,7 @@ export const javascript_visitors_runes = { value = field.kind === 'state' - ? b.call('$.source', init) + ? b.call('$.source', should_proxy(init) ? b.call('$.proxy', init) : init) : b.call('$.derived', b.thunk(init)); } else { // if no arguments, we know it's state as `$derived()` is a compile error @@ -211,16 +211,28 @@ export const javascript_visitors_runes = { } const args = /** @type {import('estree').CallExpression} */ (init).arguments; - const value = + let value = args.length === 0 ? b.id('undefined') : /** @type {import('estree').Expression} */ (visit(args[0])); - const opts = args[1] && /** @type {import('estree').Expression} */ (visit(args[1])); if (declarator.id.type === 'Identifier') { - const callee = rune === '$state' ? '$.source' : '$.derived'; - const arg = rune === '$state' ? value : b.thunk(value); - declarations.push(b.declarator(declarator.id, b.call(callee, arg, opts))); + if (rune === '$state') { + const binding = /** @type {import('#compiler').Binding} */ ( + state.scope.get(declarator.id.name) + ); + if (should_proxy(value)) { + value = b.call('$.proxy', value); + } + + if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) { + value = b.call('$.source', value); + } + } else { + value = b.call('$.derived', b.thunk(value)); + } + + declarations.push(b.declarator(declarator.id, value)); continue; } 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 ef281d319781..69421c2c4abb 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 @@ -27,6 +27,7 @@ import { DOMBooleanAttributes, EACH_INDEX_REACTIVE, EACH_IS_CONTROLLED, + EACH_IS_IMMUTABLE, EACH_ITEM_REACTIVE, EACH_KEYED } from '../../../../../constants.js'; @@ -767,18 +768,23 @@ function serialize_inline_component(node, component_name, context) { const [, value] = serialize_attribute_value(attribute.value, context); if (attribute.metadata.dynamic) { + let arg = value; + const contains_call_expression = Array.isArray(attribute.value) && attribute.value.some((n) => { return n.type === 'ExpressionTag' && n.metadata.contains_call_expression; }); + if (contains_call_expression) { const id = b.id(context.state.scope.generate(attribute.name)); context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value)))); - push_prop(b.get(attribute.name, [b.return(b.call('$.get', id))])); - } else { - push_prop(b.get(attribute.name, [b.return(value)])); + arg = b.call('$.get', id); } + + if (context.state.options.dev) arg = b.call('$.readonly', arg); + + push_prop(b.get(attribute.name, [b.return(arg)])); } else { push_prop(b.init(attribute.name, value)); } @@ -2128,14 +2134,13 @@ export const template_visitors = { // array or use nested reactivity through runes. // TODO this feels a bit "hidden performance boost"-style, investigate if there's a way // to make this apply in more cases - /** @type {number} */ - let each_type; + let each_type = 0; if ( node.key && (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index) ) { - each_type = EACH_KEYED; + each_type |= EACH_KEYED; if ( node.key.type === 'Identifier' && node.context.type === 'Identifier' && @@ -2152,12 +2157,17 @@ export const template_visitors = { each_type |= EACH_INDEX_REACTIVE; } } else { - each_type = EACH_ITEM_REACTIVE; + each_type |= EACH_ITEM_REACTIVE; } + if (each_node_meta.is_controlled) { each_type |= EACH_IS_CONTROLLED; } + if (context.state.analysis.immutable) { + each_type |= EACH_IS_IMMUTABLE; + } + // Find the parent each blocks which contain the arrays to invalidate // TODO decide how much of this we want to keep for runes mode. For now we're bailing out below const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 0bbd0944986c..798e58ed10e8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -2094,7 +2094,7 @@ export function server_component(analysis, options) { } const component_block = b.block([ - b.stmt(b.call('$.push', b.literal(analysis.runes), ...(options.immutable ? [b.true] : []))), + b.stmt(b.call('$.push', b.literal(analysis.runes))), .../** @type {import('estree').Statement[]} */ (instance.body), .../** @type {import('estree').Statement[]} */ (template.body), b.stmt(b.call('$.pop')) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index e8e56bfe78c3..f35ef1b20b00 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -105,7 +105,8 @@ export class Scope { is_called: false, prop_alias: null, expression: null, - mutation: null + mutation: null, + reassigned: false }; this.declarations.set(node.name, binding); this.root.conflicts.add(node.name); @@ -632,7 +633,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } else { extract_identifiers(node).forEach((identifier) => { const binding = scope.get(identifier.name); - if (binding) binding.mutated = true; + if (binding) { + binding.mutated = true; + binding.reassigned = true; + } }); } } diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 8ee2cce1701f..f2f9868e1315 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -46,6 +46,8 @@ export interface Analysis { module: Js; name: string; // TODO should this be filename? it's used in `compileModule` as well as `compile` warnings: RawWarning[]; + runes: boolean; + immutable: boolean; // TODO figure out if we can move this to ComponentAnalysis accessors: boolean; diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 04b4a9d1c3d7..6435c2020737 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -251,6 +251,7 @@ export interface Binding { * - `each`: An each block context variable * - `store_sub`: A $store value * - `legacy_reactive`: A `$:` declaration + * - `legacy_reactive_import`: An imported binding that is mutated inside the component */ kind: | 'normal' @@ -260,7 +261,8 @@ export interface Binding { | 'derived' | 'each' | 'store_sub' - | 'legacy_reactive'; + | 'legacy_reactive' + | 'legacy_reactive_import'; declaration_kind: DeclarationKind; /** * What the value was initialized with. @@ -270,6 +272,7 @@ export interface Binding { is_called: boolean; references: { node: Identifier; path: SvelteNode[] }[]; mutated: boolean; + reassigned: boolean; scope: Scope; /** For `legacy_reactive`: its reactive dependencies */ legacy_dependencies: Binding[]; diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 28a56d7c4a66..dd9bbfc17a52 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -304,3 +304,36 @@ export function get_parent(path, at) { } return /** @type {T} */ (node); } + +/** + * Returns `true` if the expression is an identifier, a literal, a function expression, + * or a logical expression that only contains simple expressions. Used to determine whether + * something needs to be treated as though accessing it could have side-effects (i.e. + * reading signals prematurely) + * @param {import('estree').Expression} node + * @returns {boolean} + */ +export function is_simple_expression(node) { + if ( + node.type === 'Literal' || + node.type === 'Identifier' || + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionExpression' + ) { + return true; + } + + if (node.type === 'ConditionalExpression') { + return ( + is_simple_expression(node.test) && + is_simple_expression(node.consequent) && + is_simple_expression(node.alternate) + ); + } + + if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') { + return is_simple_expression(node.left) && is_simple_expression(node.right); + } + + return false; +} diff --git a/packages/svelte/src/compiler/validate-options.js b/packages/svelte/src/compiler/validate-options.js index e928e9bac7e1..5752435178e4 100644 --- a/packages/svelte/src/compiler/validate-options.js +++ b/packages/svelte/src/compiler/validate-options.js @@ -72,7 +72,10 @@ export const validate_component_options = discloseVersion: boolean(true), - immutable: boolean(false), + immutable: deprecate( + 'The immutable option has been deprecated. It has no effect in runes mode.', + boolean(false) + ), legacy: object({ componentApi: boolean(false) @@ -166,6 +169,18 @@ function warn_removed(message) { }; } +/** + * @param {string} message + * @param {Validator} validator + * @returns {Validator} + */ +function deprecate(message, validator) { + return (input, keypath) => { + if (input !== undefined) warn(message); + return validator(input, keypath); + }; +} + /** * @param {Record} children * @param {boolean} [allow_unknown] diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index f9e74786e308..67c2dc333bbc 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -22,9 +22,6 @@ const runes = { `It looks like you're using the $${name} rune, but there is a local binding called ${name}. ` + `Referencing a local variable with a $ prefix will create a store subscription. Please rename ${name} to avoid the ambiguity.`, /** @param {string} name */ - 'state-not-mutated': (name) => - `${name} is declared with $state(...) but is never updated. Did you mean to create a function that changes its value?`, - /** @param {string} name */ 'non-state-reference': (name) => `${name} is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.` }; diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index a8abda2237a6..6b7cc123f5a7 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -3,6 +3,7 @@ export const EACH_INDEX_REACTIVE = 1 << 1; export const EACH_KEYED = 1 << 2; export const EACH_IS_CONTROLLED = 1 << 3; export const EACH_IS_ANIMATED = 1 << 4; +export const EACH_IS_IMMUTABLE = 1 << 6; /** List of Element events that will be delegated */ export const DelegatedEvents = [ diff --git a/packages/svelte/src/internal/client/each.js b/packages/svelte/src/internal/client/each.js new file mode 100644 index 000000000000..f3306ab3a087 --- /dev/null +++ b/packages/svelte/src/internal/client/each.js @@ -0,0 +1,803 @@ +import { + EACH_INDEX_REACTIVE, + EACH_IS_ANIMATED, + EACH_IS_CONTROLLED, + EACH_IS_IMMUTABLE, + EACH_ITEM_REACTIVE, + EACH_KEYED +} from '../../constants.js'; +import { create_each_block, create_each_item_block } from './block.js'; +import { + current_hydration_fragment, + get_hydration_fragment, + hydrate_block_anchor, + set_current_hydration_fragment +} from './hydration.js'; +import { clear_text_content, map_get, map_set } from './operations.js'; +import { STATE_SYMBOL } from './proxy/proxy.js'; +import { insert, remove } from './reconciler.js'; +import { empty } from './render.js'; +import { + destroy_signal, + execute_effect, + is_lazy_property, + lazy_property, + mutable_source, + push_destroy_fn, + render_effect, + schedule_task, + set_signal_value, + source +} from './runtime.js'; +import { trigger_transitions } from './transitions.js'; +import { is_array } from './utils.js'; + +const NEW_BLOCK = -1; +const MOVED_BLOCK = 99999999; +const LIS_BLOCK = -2; + +function no_op() {} + +/** + * @template V + * @param {Element | Comment} anchor_node + * @param {() => V[]} collection + * @param {number} flags + * @param {null | ((item: V) => string)} key_fn + * @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal) => void} render_fn + * @param {null | ((anchor: Node) => void)} fallback_fn + * @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn + * @returns {void} + */ +function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, reconcile_fn) { + const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; + const block = create_each_block(flags, anchor_node); + + /** @type {null | import('./types.js').Render} */ + let current_fallback = null; + hydrate_block_anchor(anchor_node, is_controlled); + + /** @type {V[]} */ + let array; + + /** @type {Array | null} */ + let keys = null; + + /** @type {null | import('./types.js').EffectSignal} */ + let render = null; + block.r = + /** @param {import('./types.js').Transition} transition */ + (transition) => { + const fallback = /** @type {import('./types.js').Render} */ (current_fallback); + const transitions = fallback.s; + transitions.add(transition); + transition.f(() => { + transitions.delete(transition); + if (transitions.size === 0) { + if (fallback.e !== null) { + if (fallback.d !== null) { + remove(fallback.d); + fallback.d = null; + } + destroy_signal(fallback.e); + fallback.e = null; + } + } + }); + }; + + const create_fallback_effect = () => { + /** @type {import('./types.js').Render} */ + const fallback = { + d: null, + e: null, + s: new Set(), + p: current_fallback + }; + // Managed effect + const effect = render_effect( + () => { + const dom = block.d; + if (dom !== null) { + remove(dom); + block.d = null; + } + let anchor = block.a; + const is_controlled = (block.f & EACH_IS_CONTROLLED) !== 0; + if (is_controlled) { + anchor = empty(); + block.a.appendChild(anchor); + } + /** @type {(anchor: Node) => void} */ (fallback_fn)(anchor); + fallback.d = block.d; + block.d = null; + }, + block, + true + ); + fallback.e = effect; + current_fallback = fallback; + }; + + const each = render_effect( + () => { + /** @type {V[]} */ + const maybe_array = collection(); + array = is_array(maybe_array) + ? maybe_array + : maybe_array == null + ? [] + : Array.from(maybe_array); + if (key_fn !== null) { + keys = array.map(key_fn); + } else if ((flags & EACH_KEYED) === 0) { + array.map(no_op); + } + const length = array.length; + if (fallback_fn !== null) { + if (length === 0) { + if (block.v.length !== 0 || render === null) { + create_fallback_effect(); + } + } else if (block.v.length === 0 && current_fallback !== null) { + const fallback = current_fallback; + const transitions = fallback.s; + if (transitions.size === 0) { + if (fallback.d !== null) { + remove(fallback.d); + fallback.d = null; + } + } else { + trigger_transitions(transitions, 'out'); + } + } + } + if (render !== null) { + execute_effect(render); + } + }, + block, + false + ); + + render = render_effect( + /** @param {import('./types.js').EachBlock} block */ + (block) => { + const flags = block.f; + const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; + const anchor_node = block.a; + reconcile_fn(array, block, anchor_node, is_controlled, render_fn, flags, true, keys); + }, + block, + true + ); + + push_destroy_fn(each, () => { + const flags = block.f; + const anchor_node = block.a; + const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; + let fallback = current_fallback; + while (fallback !== null) { + const dom = fallback.d; + if (dom !== null) { + remove(dom); + } + const effect = fallback.e; + if (effect !== null) { + destroy_signal(effect); + } + fallback = fallback.p; + } + // Clear the array + reconcile_fn([], block, anchor_node, is_controlled, render_fn, flags, false, keys); + destroy_signal(/** @type {import('./types.js').EffectSignal} */ (render)); + }); + + block.e = each; +} + +/** + * @template V + * @param {Element | Comment} anchor_node + * @param {() => V[]} collection + * @param {number} flags + * @param {null | ((item: V) => string)} key_fn + * @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal) => void} render_fn + * @param {null | ((anchor: Node) => void)} fallback_fn + * @returns {void} + */ +export function each_keyed(anchor_node, collection, flags, key_fn, render_fn, fallback_fn) { + each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, reconcile_tracked_array); +} + +/** + * @template V + * @param {Element | Comment} anchor_node + * @param {() => V[]} collection + * @param {number} flags + * @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal) => void} render_fn + * @param {null | ((anchor: Node) => void)} fallback_fn + * @returns {void} + */ +export function each_indexed(anchor_node, collection, flags, render_fn, fallback_fn) { + each(anchor_node, collection, flags, null, render_fn, fallback_fn, reconcile_indexed_array); +} + +/** + * @template V + * @param {Array} array + * @param {import('./types.js').EachBlock} each_block + * @param {Element | Comment | Text} dom + * @param {boolean} is_controlled + * @param {(anchor: null, item: V, index: number | import('./types.js').Signal) => void} render_fn + * @param {number} flags + * @param {boolean} apply_transitions + * @returns {void} + */ +function reconcile_indexed_array( + array, + each_block, + dom, + is_controlled, + render_fn, + flags, + apply_transitions +) { + var is_proxied_array = STATE_SYMBOL in array; + var a_blocks = each_block.v; + var active_transitions = each_block.s; + + if (is_proxied_array) { + flags &= ~EACH_ITEM_REACTIVE; + } + + /** @type {number | void} */ + var a = a_blocks.length; + + /** @type {number} */ + var b = array.length; + var length = Math.max(a, b); + var index = 0; + + /** @type {Array} */ + var b_blocks; + var block; + + if (active_transitions.length !== 0) { + destroy_active_transition_blocks(active_transitions); + } + + if (b === 0) { + b_blocks = []; + // Remove old blocks + if (is_controlled && a !== 0) { + clear_text_content(dom); + } + while (index < length) { + block = a_blocks[index++]; + destroy_each_item_block(block, active_transitions, apply_transitions, is_controlled); + } + } else { + var item; + b_blocks = Array(b); + if (current_hydration_fragment !== null) { + /** @type {Node} */ + var hydrating_node = current_hydration_fragment[0]; + for (; index < length; index++) { + // Hydrate block + item = is_proxied_array ? lazy_property(array, index) : array[index]; + var fragment = /** @type {Array} */ ( + get_hydration_fragment(hydrating_node) + ); + set_current_hydration_fragment(fragment); + hydrating_node = /** @type {Node} */ ( + /** @type {Node} */ (/** @type {Node} */ (fragment.at(-1)).nextSibling).nextSibling + ); + block = each_item_block(item, null, index, render_fn, flags); + b_blocks[index] = block; + } + } else { + for (; index < length; index++) { + if (index >= a) { + // Add block + item = is_proxied_array ? lazy_property(array, index) : array[index]; + block = each_item_block(item, null, index, render_fn, flags); + b_blocks[index] = block; + insert_each_item_block(block, dom, is_controlled, null); + } else if (index >= b) { + // Remove block + block = a_blocks[index]; + destroy_each_item_block(block, active_transitions, apply_transitions); + } else { + // Update block + item = array[index]; + block = a_blocks[index]; + b_blocks[index] = block; + update_each_item_block(block, item, index, flags); + } + } + } + } + + each_block.v = b_blocks; +} +// Reconcile arrays by the equality of the elements in the array. This algorithm +// is based on Ivi's reconcilation logic: +// +// https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968 +// + +/** + * @template V + * @param {Array} array + * @param {import('./types.js').EachBlock} each_block + * @param {Element | Comment | Text} dom + * @param {boolean} is_controlled + * @param {(anchor: null, item: V, index: number | import('./types.js').Signal) => void} render_fn + * @param {number} flags + * @param {boolean} apply_transitions + * @param {Array | null} keys + * @returns {void} + */ +function reconcile_tracked_array( + array, + each_block, + dom, + is_controlled, + render_fn, + flags, + apply_transitions, + keys +) { + var a_blocks = each_block.v; + const is_computed_key = keys !== null; + var is_proxied_array = STATE_SYMBOL in array; + var active_transitions = each_block.s; + + if (is_proxied_array) { + flags &= ~EACH_ITEM_REACTIVE; + } + + /** @type {number | void} */ + var a = a_blocks.length; + + /** @type {number} */ + var b = array.length; + + /** @type {Array} */ + var b_blocks; + var block; + + if (active_transitions.length !== 0) { + destroy_active_transition_blocks(active_transitions); + } + + if (b === 0) { + b_blocks = []; + // Remove old blocks + if (is_controlled && a !== 0) { + clear_text_content(dom); + } + while (a > 0) { + block = a_blocks[--a]; + destroy_each_item_block(block, active_transitions, apply_transitions, is_controlled); + } + } else { + var a_end = a - 1; + var b_end = b - 1; + var key; + var item; + var idx; + b_blocks = Array(b); + if (current_hydration_fragment !== null) { + var fragment; + + /** @type {Node} */ + var hydrating_node = current_hydration_fragment[0]; + while (b > 0) { + // Hydrate block + idx = b_end - --b; + item = array[idx]; + key = is_computed_key ? keys[idx] : item; + fragment = /** @type {Array} */ ( + get_hydration_fragment(hydrating_node) + ); + set_current_hydration_fragment(fragment); + // Get the tag of the next item in the list + // The fragment array can be empty if each block has no content + hydrating_node = /** @type {Node} */ ( + /** @type {Node} */ ((fragment.at(-1) || hydrating_node).nextSibling).nextSibling + ); + block = each_item_block(item, key, idx, render_fn, flags); + b_blocks[idx] = block; + } + } else if (a === 0) { + // Create new blocks + while (b > 0) { + idx = b_end - --b; + item = array[idx]; + key = is_computed_key ? keys[idx] : item; + block = each_item_block(item, key, idx, render_fn, flags); + b_blocks[idx] = block; + insert_each_item_block(block, dom, is_controlled, null); + } + } else { + var should_update_block = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; + var start = 0; + + /** @type {null | Text | Element | Comment} */ + var sibling = null; + item = array[b_end]; + key = is_computed_key ? keys[b_end] : item; + // Step 1 + outer: while (true) { + // From the end + while (a_blocks[a_end].k === key) { + block = a_blocks[a_end--]; + item = array[b_end]; + if (should_update_block) { + update_each_item_block(block, item, b_end, flags); + } + sibling = get_first_child(block); + b_blocks[b_end] = block; + if (start > --b_end || start > a_end) { + break outer; + } + key = is_computed_key ? keys[b_end] : item; + } + item = array[start]; + key = is_computed_key ? keys[start] : item; + // At the start + while (start <= a_end && start <= b_end && a_blocks[start].k === key) { + item = array[start]; + block = a_blocks[start]; + if (should_update_block) { + update_each_item_block(block, item, start, flags); + } + b_blocks[start] = block; + ++start; + key = is_computed_key ? keys[start] : array[start]; + } + break; + } + // Step 2 + if (start > a_end) { + while (b_end >= start) { + item = array[b_end]; + key = is_computed_key ? keys[b_end] : item; + block = each_item_block(item, key, b_end, render_fn, flags); + b_blocks[b_end--] = block; + sibling = insert_each_item_block(block, dom, is_controlled, sibling); + } + } else if (start > b_end) { + b = start; + do { + if ((block = a_blocks[b++]) !== null) { + destroy_each_item_block(block, active_transitions, apply_transitions); + } + } while (b <= a_end); + } else { + // Step 3 + var pos = 0; + var b_length = b_end - start + 1; + var sources = new Int32Array(b_length); + var item_index = new Map(); + for (b = 0; b < b_length; ++b) { + a = b + start; + sources[b] = NEW_BLOCK; + item = array[a]; + key = is_computed_key ? keys[a] : item; + map_set(item_index, key, a); + } + for (b = start; b <= a_end; ++b) { + a = map_get(item_index, /** @type {V} */ (a_blocks[b].k)); + block = a_blocks[b]; + if (a !== undefined) { + pos = pos < a ? a : MOVED_BLOCK; + sources[a - start] = b; + b_blocks[a] = block; + } else if (block !== null) { + destroy_each_item_block(block, active_transitions, apply_transitions); + } + } + // Step 4 + if (pos === MOVED_BLOCK) { + mark_lis(sources); + } + // If keys are animated, we need to do updates before actual moves + var is_animated = (flags & EACH_IS_ANIMATED) !== 0; + var should_create; + if (is_animated) { + var i = b_length; + while (i-- > 0) { + b_end = i + start; + a = sources[i]; + if (pos === MOVED_BLOCK && a !== LIS_BLOCK) { + block = b_blocks[b_end]; + update_each_item_block(block, item, b_end, flags); + } + } + } + var last_block; + var last_sibling; + while (b_length-- > 0) { + b_end = b_length + start; + a = sources[b_length]; + should_create = a === -1; + item = array[b_end]; + if (should_create) { + key = is_computed_key ? keys[b_end] : item; + block = each_item_block(item, key, b_end, render_fn, flags); + } else { + block = b_blocks[b_end]; + if (!is_animated && should_update_block) { + update_each_item_block(block, item, b_end, flags); + } + } + if (should_create || (pos === MOVED_BLOCK && a !== LIS_BLOCK)) { + last_sibling = last_block === undefined ? sibling : get_first_child(last_block); + sibling = insert_each_item_block(block, dom, is_controlled, last_sibling); + } + b_blocks[b_end] = block; + last_block = block; + } + } + } + } + + each_block.v = b_blocks; +} + +/** + * Longest Increased Subsequence algorithm + * @param {Int32Array} a + * @returns {void} + */ +function mark_lis(a) { + var length = a.length; + var parent = new Int32Array(length); + var index = new Int32Array(length); + var index_length = 0; + var i = 0; + + /** @type {number} */ + var j; + + /** @type {number} */ + var k; + + /** @type {number} */ + var lo; + + /** @type {number} */ + var hi; + + // Skip -1 values at the start of the input array `a`. + for (; a[i] === NEW_BLOCK; ++i) { + /**/ + } + + index[0] = i++; + + for (; i < length; ++i) { + k = a[i]; + + if (k !== NEW_BLOCK) { + // Ignore -1 values. + j = index[index_length]; + + if (a[j] < k) { + parent[i] = j; + index[++index_length] = i; + } else { + lo = 0; + hi = index_length; + + while (lo < hi) { + j = (lo + hi) >> 1; + if (a[index[j]] < k) { + lo = j + 1; + } else { + hi = j; + } + } + + if (k < a[index[lo]]) { + if (lo > 0) { + parent[i] = index[lo - 1]; + } + index[lo] = i; + } + } + } + } + + // Mutate input array `a` and assign -2 value to all nodes that are part of LIS. + j = index[index_length]; + + while (index_length-- >= 0) { + a[j] = LIS_BLOCK; + j = parent[j]; + } +} + +/** + * @param {import('./types.js').Block} block + * @param {Element | Comment | Text} dom + * @param {boolean} is_controlled + * @param {null | Text | Element | Comment} sibling + * @returns {Text | Element | Comment} + */ +function insert_each_item_block(block, dom, is_controlled, sibling) { + var current = /** @type {import('./types.js').TemplateNode} */ (block.d); + + if (sibling === null) { + if (is_controlled) { + return insert(current, /** @type {Element} */ (dom), null); + } else { + return insert(current, /** @type {Element} */ (dom.parentNode), dom); + } + } + + return insert(current, null, sibling); +} + +/** + * @param {import('./types.js').Block} block + * @returns {Text | Element | Comment} + */ +function get_first_child(block) { + var current = block.d; + + if (is_array(current)) { + return /** @type {Text | Element | Comment} */ (current[0]); + } + + return /** @type {Text | Element | Comment} */ (current); +} + +/** + * @param {Array} active_transitions + * @returns {void} + */ +function destroy_active_transition_blocks(active_transitions) { + var length = active_transitions.length; + + if (length > 0) { + var i = 0; + var block; + var transition; + + for (; i < length; i++) { + block = active_transitions[i]; + transition = block.r; + if (transition !== null) { + block.r = null; + destroy_each_item_block(block, null, false); + } + } + + active_transitions.length = 0; + } +} + +/** + * @param {import('./types.js').Block} block + * @returns {Text | Element | Comment} + */ +function get_first_element(block) { + const current = block.d; + + if (is_array(current)) { + for (let i = 0; i < current.length; i++) { + const node = current[i]; + if (node.nodeType !== 8) { + return node; + } + } + } + + return /** @type {Text | Element | Comment} */ (current); +} + +/** + * @param {import('./types.js').EachItemBlock} block + * @param {any} item + * @param {number} index + * @param {number} type + * @returns {void} + */ +function update_each_item_block(block, item, index, type) { + if ((type & EACH_ITEM_REACTIVE) !== 0) { + set_signal_value(block.v, item); + } else if (is_lazy_property(block.v)) { + block.v.o[block.v.p] = item; + } + const transitions = block.s; + const index_is_reactive = (type & EACH_INDEX_REACTIVE) !== 0; + // Handle each item animations + if (transitions !== null && (type & EACH_KEYED) !== 0) { + let prev_index = block.i; + if (index_is_reactive) { + prev_index = /** @type {import('./types.js').Signal} */ (prev_index).v; + } + const items = block.p.v; + if (prev_index !== index && /** @type {number} */ (index) < items.length) { + const from_dom = /** @type {Element} */ (get_first_element(block)); + const from = from_dom.getBoundingClientRect(); + schedule_task(() => { + trigger_transitions(transitions, 'key', from); + }); + } + } + if (index_is_reactive) { + set_signal_value(/** @type {import('./types.js').Signal} */ (block.i), index); + } else { + block.i = index; + } +} + +/** + * @param {import('./types.js').EachItemBlock} block + * @param {null | Array} transition_block + * @param {boolean} apply_transitions + * @param {any} controlled + * @returns {void} + */ +export function destroy_each_item_block( + block, + transition_block, + apply_transitions, + controlled = false +) { + const transitions = block.s; + + if (apply_transitions && transitions !== null) { + trigger_transitions(transitions, 'out'); + if (transition_block !== null) { + transition_block.push(block); + } + } else { + const dom = block.d; + if (!controlled && dom !== null) { + remove(dom); + } + destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e)); + } +} + +/** + * @template V + * @template O + * @template P + * @param {V | import('./types.js').LazyProperty} item + * @param {unknown} key + * @param {number} index + * @param {(anchor: null, item: V, index: number | import('./types.js').Signal) => void} render_fn + * @param {number} flags + * @returns {import('./types.js').EachItemBlock} + */ +function each_item_block(item, key, index, render_fn, flags) { + const each_item_not_reactive = (flags & EACH_ITEM_REACTIVE) === 0; + + const item_value = each_item_not_reactive + ? item + : (flags & EACH_IS_IMMUTABLE) === 0 + ? mutable_source(item) + : source(item); + + const index_value = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index); + const block = create_each_item_block(item_value, index_value, key); + + const effect = render_effect( + /** @param {import('./types.js').EachItemBlock} block */ + (block) => { + render_fn(null, block.v, block.i); + }, + block, + true + ); + + block.e = effect; + return block; +} diff --git a/packages/svelte/src/internal/client/proxy/proxy.js b/packages/svelte/src/internal/client/proxy/proxy.js new file mode 100644 index 000000000000..22e0b959b6ca --- /dev/null +++ b/packages/svelte/src/internal/client/proxy/proxy.js @@ -0,0 +1,176 @@ +import { DEV } from 'esm-env'; +import { + effect_active, + get, + set, + increment, + source, + updating_derived, + UNINITIALIZED +} from '../runtime.js'; +import { define_property, get_descriptor, is_array } from '../utils.js'; +import { READONLY_SYMBOL } from './readonly.js'; + +/** @typedef {{ s: Map>; v: import('../types.js').SourceSignal; a: boolean }} Metadata */ +/** @typedef {Record & { [STATE_SYMBOL]: Metadata }} StateObject */ + +export const STATE_SYMBOL = Symbol('$state'); + +const object_prototype = Object.prototype; +const array_prototype = Array.prototype; +const get_prototype_of = Object.getPrototypeOf; +const is_frozen = Object.isFrozen; + +/** + * @template {StateObject} T + * @param {T} value + * @returns {T} + */ +export function proxy(value) { + if (typeof value === 'object' && value != null && !is_frozen(value) && !(STATE_SYMBOL in value)) { + const prototype = get_prototype_of(value); + + // TODO handle Map and Set as well + if (prototype === object_prototype || prototype === array_prototype) { + define_property(value, STATE_SYMBOL, { value: init(value), writable: false }); + + // @ts-expect-error not sure how to fix this + return new Proxy(value, handler); + } + } + + return value; +} + +/** + * @param {StateObject} value + * @returns {Metadata} + */ +function init(value) { + return { + s: new Map(), + v: source(0), + a: is_array(value) + }; +} + +/** @type {ProxyHandler} */ +const handler = { + defineProperty(target, prop, descriptor) { + if (descriptor.value) { + const metadata = target[STATE_SYMBOL]; + + const s = metadata.s.get(prop); + if (s !== undefined) set(s, proxy(descriptor.value)); + } + + return Reflect.defineProperty(target, prop, descriptor); + }, + + deleteProperty(target, prop) { + const metadata = target[STATE_SYMBOL]; + + const s = metadata.s.get(prop); + if (s !== undefined) set(s, UNINITIALIZED); + + if (prop in target) increment(metadata.v); + + return delete target[prop]; + }, + + get(target, prop, receiver) { + if (prop === READONLY_SYMBOL) return target[READONLY_SYMBOL]; + + const metadata = target[STATE_SYMBOL]; + let s = metadata.s.get(prop); + + // if we're reading a property in a reactive context, create a source, + // but only if it's an own property and not a prototype property + if ( + s === undefined && + (effect_active() || updating_derived) && + (!(prop in target) || get_descriptor(target, prop)?.writable) + ) { + s = source(proxy(target[prop])); + metadata.s.set(prop, s); + } + + const value = s !== undefined ? get(s) : Reflect.get(target, prop, receiver); + return value === UNINITIALIZED ? undefined : value; + }, + + getOwnPropertyDescriptor(target, prop) { + const descriptor = Reflect.getOwnPropertyDescriptor(target, prop); + if (descriptor && 'value' in descriptor) { + const metadata = target[STATE_SYMBOL]; + const s = metadata.s.get(prop); + + if (s) { + descriptor.value = get(s); + } + } + + return descriptor; + }, + + has(target, prop) { + if (prop === STATE_SYMBOL) { + return true; + } + const metadata = target[STATE_SYMBOL]; + const has = Reflect.has(target, prop); + + let s = metadata.s.get(prop); + if (s !== undefined || (effect_active() && (!has || get_descriptor(target, prop)?.writable))) { + if (s === undefined) { + s = source(has ? proxy(target[prop]) : UNINITIALIZED); + metadata.s.set(prop, s); + } + const value = get(s); + if (value === UNINITIALIZED) { + return false; + } + } + return has; + }, + + set(target, prop, value) { + if (prop === READONLY_SYMBOL) { + target[READONLY_SYMBOL] = value; + return true; + } + + const metadata = target[STATE_SYMBOL]; + + const s = metadata.s.get(prop); + if (s !== undefined) set(s, proxy(value)); + + if (metadata.a && prop === 'length') { + for (let i = value; i < target.length; i += 1) { + const s = metadata.s.get(i + ''); + if (s !== undefined) set(s, UNINITIALIZED); + } + } + + if (!(prop in target)) increment(metadata.v); + // @ts-ignore + target[prop] = value; + + return true; + }, + + ownKeys(target) { + const metadata = target[STATE_SYMBOL]; + + get(metadata.v); + return Reflect.ownKeys(target); + } +}; + +if (DEV) { + handler.setPrototypeOf = () => { + throw new Error('Cannot set prototype of $state object'); + }; +} + +export { readonly } from './readonly.js'; diff --git a/packages/svelte/src/internal/client/proxy/readonly.js b/packages/svelte/src/internal/client/proxy/readonly.js new file mode 100644 index 000000000000..fa698c3b0830 --- /dev/null +++ b/packages/svelte/src/internal/client/proxy/readonly.js @@ -0,0 +1,65 @@ +import { define_property, get_descriptor } from '../utils.js'; + +/** + * @template {Record} T + * @typedef {T & { [READONLY_SYMBOL]: Proxy }} StateObject + */ + +export const READONLY_SYMBOL = Symbol('readonly'); + +const object_prototype = Object.prototype; +const array_prototype = Array.prototype; +const get_prototype_of = Object.getPrototypeOf; +const is_frozen = Object.isFrozen; + +/** + * @template {Record} T + * @template {StateObject} U + * @param {U} value + * @returns {Proxy | U} + */ +export function readonly(value) { + const proxy = value && value[READONLY_SYMBOL]; + if (proxy) return proxy; + + if ( + typeof value === 'object' && + value != null && + !is_frozen(value) && + !(READONLY_SYMBOL in value) + ) { + const prototype = get_prototype_of(value); + + // TODO handle Map and Set as well + if (prototype === object_prototype || prototype === array_prototype) { + const proxy = new Proxy(value, handler); + define_property(value, READONLY_SYMBOL, { value: proxy, writable: false }); + + return proxy; + } + } + + return value; +} + +/** @returns {never} */ +const readonly_error = () => { + throw new Error(`Props are read-only, unless used with \`bind:\``); +}; + +/** @type {ProxyHandler>} */ +const handler = { + defineProperty: readonly_error, + deleteProperty: readonly_error, + set: readonly_error, + + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + + if (!(prop in target)) { + return readonly(value); + } + + return value; + } +}; diff --git a/packages/svelte/src/internal/client/reconciler.js b/packages/svelte/src/internal/client/reconciler.js index a5fcff68730a..c96d9460a941 100644 --- a/packages/svelte/src/internal/client/reconciler.js +++ b/packages/svelte/src/internal/client/reconciler.js @@ -1,17 +1,6 @@ -import { append_child, map_get, map_set, clear_text_content } from './operations.js'; -import { - current_hydration_fragment, - get_hydration_fragment, - hydrate_block_anchor, - set_current_hydration_fragment -} from './hydration.js'; +import { append_child } from './operations.js'; +import { current_hydration_fragment, hydrate_block_anchor } from './hydration.js'; import { is_array } from './utils.js'; -import { each_item_block, destroy_each_item_block, update_each_item_block } from './render.js'; -import { EACH_INDEX_REACTIVE, EACH_IS_ANIMATED, EACH_ITEM_REACTIVE } from '../../constants.js'; - -const NEW_BLOCK = -1; -const MOVED_BLOCK = 99999999; -const LIS_BLOCK = -2; /** @param {string} html */ export function create_fragment_from_html(html) { @@ -103,429 +92,3 @@ export function reconcile_html(dom, value, svg) { target.before(svg ? /** @type {Node} */ (clone.firstChild) : clone); return /** @type {Array} */ (frag_nodes); } - -/** - * @param {import('./types.js').Block} block - * @param {Element | Comment | Text} dom - * @param {boolean} is_controlled - * @param {null | Text | Element | Comment} sibling - * @returns {Text | Element | Comment} - */ -function insert_each_item_block(block, dom, is_controlled, sibling) { - var current = /** @type {import('./types.js').TemplateNode} */ (block.d); - if (sibling === null) { - if (is_controlled) { - return insert(current, /** @type {Element} */ (dom), null); - } else { - return insert(current, /** @type {Element} */ (dom.parentNode), dom); - } - } - return insert(current, null, sibling); -} - -/** - * @param {import('./types.js').Block} block - * @returns {Text | Element | Comment} - */ -function get_first_child(block) { - var current = block.d; - if (is_array(current)) { - return /** @type {Text | Element | Comment} */ (current[0]); - } - return /** @type {Text | Element | Comment} */ (current); -} - -/** - * @param {Array} active_transitions - * @returns {void} - */ -function destroy_active_transition_blocks(active_transitions) { - var length = active_transitions.length; - if (length > 0) { - var i = 0; - var block; - var transition; - for (; i < length; i++) { - block = active_transitions[i]; - transition = block.r; - if (transition !== null) { - block.r = null; - destroy_each_item_block(block, null, false); - } - } - active_transitions.length = 0; - } -} - -/** - * @template V - * @param {Array} array - * @param {import('./types.js').EachBlock} each_block - * @param {Element | Comment | Text} dom - * @param {boolean} is_controlled - * @param {(anchor: null, item: V, index: number | import('./types.js').Signal) => void} render_fn - * @param {number} flags - * @param {boolean} apply_transitions - * @returns {void} - */ -export function reconcile_indexed_array( - array, - each_block, - dom, - is_controlled, - render_fn, - flags, - apply_transitions -) { - var a_blocks = each_block.v; - var active_transitions = each_block.s; - - /** @type {number | void} */ - var a = a_blocks.length; - - /** @type {number} */ - var b = array.length; - var length = Math.max(a, b); - var index = 0; - - /** @type {Array} */ - var b_blocks; - var block; - if (active_transitions.length !== 0) { - destroy_active_transition_blocks(active_transitions); - } - if (b === 0) { - b_blocks = []; - // Remove old blocks - if (is_controlled && a !== 0) { - clear_text_content(dom); - } - while (index < length) { - block = a_blocks[index++]; - destroy_each_item_block(block, active_transitions, apply_transitions, is_controlled); - } - } else { - var item; - b_blocks = Array(b); - if (current_hydration_fragment !== null) { - /** @type {Node} */ - var hydrating_node = current_hydration_fragment[0]; - for (; index < length; index++) { - // Hydrate block - item = array[index]; - var fragment = /** @type {Array} */ ( - get_hydration_fragment(hydrating_node) - ); - set_current_hydration_fragment(fragment); - hydrating_node = /** @type {Node} */ ( - /** @type {Node} */ (/** @type {Node} */ (fragment.at(-1)).nextSibling).nextSibling - ); - block = each_item_block(item, null, index, render_fn, flags); - b_blocks[index] = block; - } - } else { - for (; index < length; index++) { - if (index >= a) { - // Add block - item = array[index]; - block = each_item_block(item, null, index, render_fn, flags); - b_blocks[index] = block; - insert_each_item_block(block, dom, is_controlled, null); - } else if (index >= b) { - // Remove block - block = a_blocks[index]; - destroy_each_item_block(block, active_transitions, apply_transitions); - } else { - // Update block - item = array[index]; - block = a_blocks[index]; - b_blocks[index] = block; - update_each_item_block(block, item, index, flags); - } - } - } - } - each_block.v = b_blocks; -} -// Reconcile arrays by the equality of the elements in the array. This algorithm -// is based on Ivi's reconcilation logic: -// -// https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968 -// - -/** - * @template V - * @param {Array} array - * @param {import('./types.js').EachBlock} each_block - * @param {Element | Comment | Text} dom - * @param {boolean} is_controlled - * @param {(anchor: null, item: V, index: number | import('./types.js').Signal) => void} render_fn - * @param {number} flags - * @param {boolean} apply_transitions - * @param {Array | null} keys - * @returns {void} - */ -export function reconcile_tracked_array( - array, - each_block, - dom, - is_controlled, - render_fn, - flags, - apply_transitions, - keys -) { - var a_blocks = each_block.v; - const is_computed_key = keys !== null; - var active_transitions = each_block.s; - - /** @type {number | void} */ - var a = a_blocks.length; - - /** @type {number} */ - var b = array.length; - - /** @type {Array} */ - var b_blocks; - var block; - if (active_transitions.length !== 0) { - destroy_active_transition_blocks(active_transitions); - } - if (b === 0) { - b_blocks = []; - // Remove old blocks - if (is_controlled && a !== 0) { - clear_text_content(dom); - } - while (a > 0) { - block = a_blocks[--a]; - destroy_each_item_block(block, active_transitions, apply_transitions, is_controlled); - } - } else { - var a_end = a - 1; - var b_end = b - 1; - var key; - var item; - var idx; - b_blocks = Array(b); - if (current_hydration_fragment !== null) { - var fragment; - - /** @type {Node} */ - var hydrating_node = current_hydration_fragment[0]; - while (b > 0) { - // Hydrate block - idx = b_end - --b; - item = array[idx]; - key = is_computed_key ? keys[idx] : item; - fragment = /** @type {Array} */ ( - get_hydration_fragment(hydrating_node) - ); - set_current_hydration_fragment(fragment); - // Get the tag of the next item in the list - // The fragment array can be empty if each block has no content - hydrating_node = /** @type {Node} */ ( - /** @type {Node} */ ((fragment.at(-1) || hydrating_node).nextSibling).nextSibling - ); - block = each_item_block(item, key, idx, render_fn, flags); - b_blocks[idx] = block; - } - } else if (a === 0) { - // Create new blocks - while (b > 0) { - idx = b_end - --b; - item = array[idx]; - key = is_computed_key ? keys[idx] : item; - block = each_item_block(item, key, idx, render_fn, flags); - b_blocks[idx] = block; - insert_each_item_block(block, dom, is_controlled, null); - } - } else { - var should_update_block = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; - var start = 0; - - /** @type {null | Text | Element | Comment} */ - var sibling = null; - item = array[b_end]; - key = is_computed_key ? keys[b_end] : item; - // Step 1 - outer: while (true) { - // From the end - while (a_blocks[a_end].k === key) { - block = a_blocks[a_end--]; - item = array[b_end]; - if (should_update_block) { - update_each_item_block(block, item, b_end, flags); - } - sibling = get_first_child(block); - b_blocks[b_end] = block; - if (start > --b_end || start > a_end) { - break outer; - } - key = is_computed_key ? keys[b_end] : item; - } - item = array[start]; - key = is_computed_key ? keys[start] : item; - // At the start - while (start <= a_end && start <= b_end && a_blocks[start].k === key) { - item = array[start]; - block = a_blocks[start]; - if (should_update_block) { - update_each_item_block(block, item, start, flags); - } - b_blocks[start] = block; - ++start; - key = is_computed_key ? keys[start] : array[start]; - } - break; - } - // Step 2 - if (start > a_end) { - while (b_end >= start) { - item = array[b_end]; - key = is_computed_key ? keys[b_end] : item; - block = each_item_block(item, key, b_end, render_fn, flags); - b_blocks[b_end--] = block; - sibling = insert_each_item_block(block, dom, is_controlled, sibling); - } - } else if (start > b_end) { - b = start; - do { - if ((block = a_blocks[b++]) !== null) { - destroy_each_item_block(block, active_transitions, apply_transitions); - } - } while (b <= a_end); - } else { - // Step 3 - var pos = 0; - var b_length = b_end - start + 1; - var sources = new Int32Array(b_length); - var item_index = new Map(); - for (b = 0; b < b_length; ++b) { - a = b + start; - sources[b] = NEW_BLOCK; - item = array[a]; - key = is_computed_key ? keys[a] : item; - map_set(item_index, key, a); - } - for (b = start; b <= a_end; ++b) { - a = map_get(item_index, /** @type {V} */ (a_blocks[b].k)); - block = a_blocks[b]; - if (a !== undefined) { - pos = pos < a ? a : MOVED_BLOCK; - sources[a - start] = b; - b_blocks[a] = block; - } else if (block !== null) { - destroy_each_item_block(block, active_transitions, apply_transitions); - } - } - // Step 4 - if (pos === MOVED_BLOCK) { - mark_lis(sources); - } - // If keys are animated, we need to do updates before actual moves - var is_animated = (flags & EACH_IS_ANIMATED) !== 0; - var should_create; - if (is_animated) { - var i = b_length; - while (i-- > 0) { - b_end = i + start; - a = sources[i]; - if (pos === MOVED_BLOCK && a !== LIS_BLOCK) { - block = b_blocks[b_end]; - update_each_item_block(block, item, b_end, flags); - } - } - } - var last_block; - var last_sibling; - while (b_length-- > 0) { - b_end = b_length + start; - a = sources[b_length]; - should_create = a === -1; - item = array[b_end]; - if (should_create) { - key = is_computed_key ? keys[b_end] : item; - block = each_item_block(item, key, b_end, render_fn, flags); - } else { - block = b_blocks[b_end]; - if (!is_animated && should_update_block) { - update_each_item_block(block, item, b_end, flags); - } - } - if (should_create || (pos === MOVED_BLOCK && a !== LIS_BLOCK)) { - last_sibling = last_block === undefined ? sibling : get_first_child(last_block); - sibling = insert_each_item_block(block, dom, is_controlled, last_sibling); - } - b_blocks[b_end] = block; - last_block = block; - } - } - } - } - each_block.v = b_blocks; -} -// Longest Increased Subsequence algorithm. - -/** - * @param {Int32Array} a - * @returns {void} - */ -function mark_lis(a) { - var length = a.length; - var parent = new Int32Array(length); - var index = new Int32Array(length); - var index_length = 0; - var i = 0; - - /** @type {number} */ - var j; - - /** @type {number} */ - var k; - - /** @type {number} */ - var lo; - - /** @type {number} */ - var hi; - // Skip -1 values at the start of the input array `a`. - for (; a[i] === NEW_BLOCK; ++i) { - /**/ - } - index[0] = i++; - for (; i < length; ++i) { - k = a[i]; - if (k !== NEW_BLOCK) { - // Ignore -1 values. - j = index[index_length]; - if (a[j] < k) { - parent[i] = j; - index[++index_length] = i; - } else { - lo = 0; - hi = index_length; - while (lo < hi) { - j = (lo + hi) >> 1; - if (a[index[j]] < k) { - lo = j + 1; - } else { - hi = j; - } - } - if (k < a[index[lo]]) { - if (lo > 0) { - parent[i] = index[lo - 1]; - } - index[lo] = i; - } - } - } - } - // Mutate input array `a` and assign -2 value to all nodes that are part of LIS. - j = index[index_length]; - while (index_length-- >= 0) { - a[j] = LIS_BLOCK; - j = parent[j]; - } -} diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index c23b38de5a05..2111f412e475 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -11,8 +11,6 @@ import { } from './operations.js'; import { create_root_block, - create_each_item_block, - create_each_block, create_if_block, create_key_block, create_await_block, @@ -21,23 +19,8 @@ import { create_dynamic_component_block, create_snippet_block } from './block.js'; -import { - EACH_KEYED, - EACH_IS_CONTROLLED, - EACH_INDEX_REACTIVE, - EACH_ITEM_REACTIVE, - PassiveDelegatedEvents, - DelegatedEvents, - AttributeAliases -} from '../../constants.js'; -import { - create_fragment_from_html, - insert, - reconcile_tracked_array, - reconcile_html, - remove, - reconcile_indexed_array -} from './reconciler.js'; +import { PassiveDelegatedEvents, DelegatedEvents, AttributeAliases } from '../../constants.js'; +import { create_fragment_from_html, insert, reconcile_html, remove } from './reconciler.js'; import { render_effect, destroy_signal, @@ -54,14 +37,13 @@ import { expose, safe_not_equal, current_block, - set_signal_value, source, managed_effect, - safe_equal, push, current_component_context, pop, - schedule_task + unwrap, + mutable_source } from './runtime.js'; import { current_hydration_fragment, @@ -2019,288 +2001,6 @@ export function key(anchor_node, key, render_fn) { block.e = key_effect; } -/** - * @param {import('./types.js').Block} block - * @returns {Text | Element | Comment} - */ -function get_first_element(block) { - const current = block.d; - if (is_array(current)) { - for (let i = 0; i < current.length; i++) { - const node = current[i]; - if (node.nodeType !== 8) { - return node; - } - } - } - return /** @type {Text | Element | Comment} */ (current); -} - -/** - * @param {import('./types.js').EachItemBlock} block - * @param {any} item - * @param {number} index - * @param {number} type - * @returns {void} - */ -export function update_each_item_block(block, item, index, type) { - if ((type & EACH_ITEM_REACTIVE) !== 0) { - set_signal_value(block.v, item); - } - const transitions = block.s; - const index_is_reactive = (type & EACH_INDEX_REACTIVE) !== 0; - // Handle each item animations - if (transitions !== null && (type & EACH_KEYED) !== 0) { - let prev_index = block.i; - if (index_is_reactive) { - prev_index = /** @type {import('./types.js').Signal} */ (prev_index).v; - } - const items = block.p.v; - if (prev_index !== index && /** @type {number} */ (index) < items.length) { - const from_dom = /** @type {Element} */ (get_first_element(block)); - const from = from_dom.getBoundingClientRect(); - schedule_task(() => { - trigger_transitions(transitions, 'key', from); - }); - } - } - if (index_is_reactive) { - set_signal_value(/** @type {import('./types.js').Signal} */ (block.i), index); - } else { - block.i = index; - } -} - -/** - * @param {import('./types.js').EachItemBlock} block - * @param {null | Array} transition_block - * @param {boolean} apply_transitions - * @param {any} controlled - * @returns {void} - */ -export function destroy_each_item_block( - block, - transition_block, - apply_transitions, - controlled = false -) { - const transitions = block.s; - if (apply_transitions && transitions !== null) { - trigger_transitions(transitions, 'out'); - if (transition_block !== null) { - transition_block.push(block); - } - } else { - const dom = block.d; - if (!controlled && dom !== null) { - remove(dom); - } - destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e)); - } -} - -/** - * @template V - * @param {V} item - * @param {unknown} key - * @param {number} index - * @param {(anchor: null, item: V, index: number | import('./types.js').Signal) => void} render_fn - * @param {number} flags - * @returns {import('./types.js').EachItemBlock} - */ -export function each_item_block(item, key, index, render_fn, flags) { - const item_value = (flags & EACH_ITEM_REACTIVE) === 0 ? item : source(item); - const index_value = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index); - const block = create_each_item_block(item_value, index_value, key); - const effect = render_effect( - /** @param {import('./types.js').EachItemBlock} block */ - (block) => { - render_fn(null, block.v, block.i); - }, - block, - true - ); - block.e = effect; - return block; -} - -/** - * @template V - * @param {Element | Comment} anchor_node - * @param {() => V[]} collection - * @param {number} flags - * @param {null | ((item: V) => string)} key_fn - * @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal) => void} render_fn - * @param {null | ((anchor: Node) => void)} fallback_fn - * @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn - * @returns {void} - */ -function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, reconcile_fn) { - const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; - const block = create_each_block(flags, anchor_node); - - /** @type {null | import('./types.js').Render} */ - let current_fallback = null; - hydrate_block_anchor(anchor_node, is_controlled); - - /** @type {V[]} */ - let array; - - /** @type {Array | null} */ - let keys = null; - - /** @type {null | import('./types.js').EffectSignal} */ - let render = null; - block.r = - /** @param {import('./types.js').Transition} transition */ - (transition) => { - const fallback = /** @type {import('./types.js').Render} */ (current_fallback); - const transitions = fallback.s; - transitions.add(transition); - transition.f(() => { - transitions.delete(transition); - if (transitions.size === 0) { - if (fallback.e !== null) { - if (fallback.d !== null) { - remove(fallback.d); - fallback.d = null; - } - destroy_signal(fallback.e); - fallback.e = null; - } - } - }); - }; - const create_fallback_effect = () => { - /** @type {import('./types.js').Render} */ - const fallback = { - d: null, - e: null, - s: new Set(), - p: current_fallback - }; - // Managed effect - const effect = render_effect( - () => { - const dom = block.d; - if (dom !== null) { - remove(dom); - block.d = null; - } - let anchor = block.a; - const is_controlled = (block.f & EACH_IS_CONTROLLED) !== 0; - if (is_controlled) { - anchor = empty(); - block.a.appendChild(anchor); - } - /** @type {(anchor: Node) => void} */ (fallback_fn)(anchor); - fallback.d = block.d; - block.d = null; - }, - block, - true - ); - fallback.e = effect; - current_fallback = fallback; - }; - const each = render_effect( - () => { - /** @type {V[]} */ - const maybe_array = collection(); - array = is_array(maybe_array) - ? maybe_array - : maybe_array == null - ? [] - : Array.from(maybe_array); - if (key_fn !== null) { - keys = array.map(key_fn); - } - if (fallback_fn !== null) { - if (array.length === 0) { - if (block.v.length !== 0 || render === null) { - create_fallback_effect(); - } - } else if (block.v.length === 0 && current_fallback !== null) { - const fallback = current_fallback; - const transitions = fallback.s; - if (transitions.size === 0) { - if (fallback.d !== null) { - remove(fallback.d); - fallback.d = null; - } - } else { - trigger_transitions(transitions, 'out'); - } - } - } - if (render !== null) { - execute_effect(render); - } - }, - block, - false - ); - render = render_effect( - /** @param {import('./types.js').EachBlock} block */ - (block) => { - const flags = block.f; - const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; - const anchor_node = block.a; - reconcile_fn(array, block, anchor_node, is_controlled, render_fn, flags, true, keys); - }, - block, - true - ); - push_destroy_fn(each, () => { - const flags = block.f; - const anchor_node = block.a; - const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; - let fallback = current_fallback; - while (fallback !== null) { - const dom = fallback.d; - if (dom !== null) { - remove(dom); - } - const effect = fallback.e; - if (effect !== null) { - destroy_signal(effect); - } - fallback = fallback.p; - } - // Clear the array - reconcile_fn([], block, anchor_node, is_controlled, render_fn, flags, false, keys); - destroy_signal(/** @type {import('./types.js').EffectSignal} */ (render)); - }); - block.e = each; -} - -/** - * @template V - * @param {Element | Comment} anchor_node - * @param {() => V[]} collection - * @param {number} flags - * @param {null | ((item: V) => string)} key_fn - * @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal) => void} render_fn - * @param {null | ((anchor: Node) => void)} fallback_fn - * @returns {void} - */ -export function each_keyed(anchor_node, collection, flags, key_fn, render_fn, fallback_fn) { - each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, reconcile_tracked_array); -} - -/** - * @template V - * @param {Element | Comment} anchor_node - * @param {() => V[]} collection - * @param {number} flags - * @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal) => void} render_fn - * @param {null | ((anchor: Node) => void)} fallback_fn - * @returns {void} - */ -export function each_indexed(anchor_node, collection, flags, render_fn, fallback_fn) { - each(anchor_node, collection, flags, null, render_fn, fallback_fn, reconcile_indexed_array); -} - /** * @param {Element | Text | Comment} anchor * @param {boolean} is_html @@ -2890,20 +2590,6 @@ export function spread_props(props) { return merged_props; } -/** - * @template V - * @param {V} value - * @returns {import('./types.js').UnwrappedSignal} - */ -export function unwrap(value) { - if (is_signal(value)) { - // @ts-ignore - return get(value); - } - // @ts-ignore - return value; -} - /** * Mounts the given component to the given target and returns a handle to the component's public accessors * as well as a `$set` and `$destroy` method to update the props of the component or destroy it. @@ -2920,7 +2606,6 @@ export function unwrap(value) { * events?: Events; * context?: Map; * intro?: boolean; - * immutable?: boolean; * recover?: false; * }} options * @returns {Exports & { $destroy: () => void; $set: (props: Partial) => void; }} @@ -2938,15 +2623,7 @@ export function createRoot(component, options) { * @param {any} value */ function add_prop(name, value) { - const prop = source( - value, - options.immutable - ? /** - * @param {any} a - * @param {any} b - */ (a, b) => a === b - : safe_equal - ); + const prop = source(value); _sources[name] = prop; define_property(_props, name, { get() { @@ -2980,11 +2657,9 @@ export function createRoot(component, options) { return _props[property]; } }); - const props_source = source( - props_proxy, - // We're resetting the same proxy instance for updates, therefore bypass equality checks - () => false - ); + + // We're resetting the same proxy instance for updates, therefore bypass equality checks + const props_source = mutable_source(props_proxy); let [accessors, $destroy] = mount(component, { ...options, @@ -3040,7 +2715,6 @@ export function createRoot(component, options) { * events?: Events; * context?: Map; * intro?: boolean; - * immutable?: boolean; * recover?: false; * }} options * @returns {[Exports, () => void]} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6b3d7abecc19..2d3e5204c733 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,7 +1,6 @@ import { DEV } from 'esm-env'; import { subscribe_to_store } from '../../store/utils.js'; import { EMPTY_FUNC, run_all } from '../common.js'; -import { unwrap } from './render.js'; import { get_descriptors, is_array } from './utils.js'; export const SOURCE = 1; @@ -24,6 +23,7 @@ const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; export const UNINITIALIZED = Symbol(); +export const LAZY_PROPERTY = Symbol(); // Used for controlling the flush of effects. let current_scheduler_mode = FLUSH_MICROTASK; @@ -87,6 +87,8 @@ export let current_block = null; export let current_component_context = null; export let is_ssr = false; +export let updating_derived = false; + /** * @param {boolean} ssr * @returns {void} @@ -108,8 +110,6 @@ export function create_component_context(props) { c: null, // effects e: null, - // immutable - i: false, // mounted m: false, // parent @@ -145,7 +145,7 @@ export function set_current_component_context(context_stack_item) { * @param {unknown} b * @returns {boolean} */ -function default_equals(a, b) { +export function default_equals(a, b) { return a === b; } @@ -161,7 +161,7 @@ function create_source_signal(flags, value) { // consumers c: null, // equals - e: null, + e: default_equals, // flags f: flags, // value @@ -176,7 +176,7 @@ function create_source_signal(flags, value) { // consumers c: null, // equals - e: null, + e: default_equals, // flags f: flags, // value @@ -688,7 +688,10 @@ export async function tick() { * @returns {void} */ function update_derived(signal, force_schedule) { + const previous_updating_derived = updating_derived; + updating_derived = true; const value = execute_signal_fn(signal); + updating_derived = previous_updating_derived; const status = current_skip_consumer || (current_effect === null && (signal.f & UNOWNED) !== 0) ? DIRTY @@ -726,7 +729,7 @@ export function store_get(store, store_name, stores) { entry = { store: null, last_value: null, - value: source(UNINITIALIZED), + value: mutable_source(UNINITIALIZED), unsubscribe: EMPTY_FUNC }; // TODO: can we remove this code? it was refactored out when we split up source/comptued signals @@ -1157,11 +1160,10 @@ export function destroy_signal(signal) { /** * @template V * @param {() => V} init - * @param {import('./types.js').EqualsFunctions} [equals] * @returns {import('./types.js').ComputationSignal} */ /*#__NO_SIDE_EFFECTS__*/ -export function derived(init, equals) { +export function derived(init) { const is_unowned = current_effect === null; const flags = is_unowned ? DERIVED | UNOWNED : DERIVED; const signal = /** @type {import('./types.js').ComputationSignal} */ ( @@ -1169,7 +1171,7 @@ export function derived(init, equals) { ); signal.i = init; signal.x = current_component_context; - signal.e = get_equals_method(equals); + signal.e = default_equals; if (!is_unowned) { push_reference(/** @type {import('./types.js').EffectSignal} */ (current_effect), signal); } @@ -1179,30 +1181,25 @@ export function derived(init, equals) { /** * @template V * @param {V} initial_value - * @param {import('./types.js').EqualsFunctions} [equals] * @returns {import('./types.js').SourceSignal} */ /*#__NO_SIDE_EFFECTS__*/ -export function source(initial_value, equals) { +export function source(initial_value) { const source = create_source_signal(SOURCE | CLEAN, initial_value); source.x = current_component_context; - source.e = get_equals_method(equals); return source; } /** - * @param {import('./types.js').EqualsFunctions} [equals] - * @returns {import('./types.js').EqualsFunctions} + * @template V + * @param {V} initial_value + * @returns {import('./types.js').SourceSignal} */ -function get_equals_method(equals) { - if (equals !== undefined) { - return equals; - } - const context = current_component_context; - if (context && !context.i) { - return safe_equal; - } - return default_equals; +/*#__NO_SIDE_EFFECTS__*/ +export function mutable_source(initial_value) { + const s = source(initial_value); + s.e = safe_equal; + return s; } /** @@ -1425,6 +1422,20 @@ export function is_signal(val) { ); } +/** + * @template O + * @template P + * @param {any} val + * @returns {val is import('./types.js').LazyProperty} + */ +export function is_lazy_property(val) { + return ( + typeof val === 'object' && + val !== null && + /** @type {import('./types.js').LazyProperty} */ (val).t === LAZY_PROPERTY + ); +} + /** * @template V * @param {unknown} val @@ -1452,11 +1463,12 @@ export function is_store(val) { * @template V * @param {import('./types.js').MaybeSignal>} props_obj * @param {string} key + * @param {boolean} immutable * @param {V | (() => V)} [default_value] * @param {boolean} [call_default_value] * @returns {import('./types.js').Signal | (() => V)} */ -export function prop_source(props_obj, key, default_value, call_default_value) { +export function prop_source(props_obj, key, immutable, default_value, call_default_value) { const props = is_signal(props_obj) ? get(props_obj) : props_obj; const possible_signal = /** @type {import('./types.js').MaybeSignal} */ ( expose(() => props[key]) @@ -1468,8 +1480,7 @@ export function prop_source(props_obj, key, default_value, call_default_value) { if ( is_signal(possible_signal) && possible_signal.v === value && - update_bound_prop === undefined && - get_equals_method() === possible_signal.e + update_bound_prop === undefined ) { if (should_set_default_value) { set( @@ -1487,13 +1498,11 @@ export function prop_source(props_obj, key, default_value, call_default_value) { call_default_value ? default_value() : default_value; } - const source_signal = source(value); + const source_signal = immutable ? source(value) : mutable_source(value); // Synchronize prop changes with source signal. // Needs special equality checking because the prop in the // parent could be changed through `foo.bar = 'new value'`. - const immutable = /** @type {import('./types.js').ComponentContext} */ (current_component_context) - .i; let ignore_next1 = false; let ignore_next2 = false; let did_update_to_defined = !should_set_default_value; @@ -1552,16 +1561,12 @@ export function prop_source(props_obj, key, default_value, call_default_value) { /** * If the prop is readonly and has no fallback value, we can use this function, else we need to use `prop_source`. - * @template V * @param {import('./types.js').MaybeSignal>} props_obj * @param {string} key * @returns {any} */ export function prop(props_obj, key) { - return () => { - const props = is_signal(props_obj) ? get(props_obj) : props_obj; - return /** @type {V} */ (props[key]); - }; + return is_signal(props_obj) ? () => get(props_obj)[key] : () => props_obj[key]; } /** @@ -1804,13 +1809,11 @@ export function onDestroy(fn) { /** * @param {import('./types.js').MaybeSignal>} props * @param {any} runes - * @param {any} immutable * @returns {void} */ -export function push(props, runes = false, immutable = false) { +export function push(props, runes = false) { const context_stack_item = create_component_context(props); context_stack_item.r = runes; - context_stack_item.i = immutable; current_component_context = context_stack_item; } @@ -1901,3 +1904,35 @@ export function inspect(get_value, inspect = console.log) { }; }); } + +/** + * @template O + * @template P + * @param {O} o + * @param {P} p + * @returns {import('./types.js').LazyProperty} + */ +export function lazy_property(o, p) { + return { + o, + p, + t: LAZY_PROPERTY + }; +} + +/** + * @template V + * @param {V} value + * @returns {import('./types.js').UnwrappedSignal} + */ +export function unwrap(value) { + if (is_signal(value)) { + // @ts-ignore + return get(value); + } + if (is_lazy_property(value)) { + return value.o[value.p]; + } + // @ts-ignore + return value; +} diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index 8b2666a7132d..27db05fdd236 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -9,8 +9,9 @@ import { KEY_BLOCK, ROOT_BLOCK } from './block.js'; +import { destroy_each_item_block } from './each.js'; import { append_child } from './operations.js'; -import { destroy_each_item_block, empty } from './render.js'; +import { empty } from './render.js'; import { current_block, current_effect, diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index ee7bc6eea10a..13bfbc16cd7b 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -10,7 +10,7 @@ import { DYNAMIC_ELEMENT_BLOCK, SNIPPET_BLOCK } from './block.js'; -import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT } from './runtime.js'; +import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT, LAZY_PROPERTY } from './runtime.js'; // Put all internal types in this file. Once we convert to JSDoc, we can make this a d.ts file @@ -47,8 +47,6 @@ export type ComponentContext = { p: null | ComponentContext; /** context */ c: null | Map; - /** immutable */ - i: boolean; /** runes */ r: boolean; /** update_callbacks */ @@ -116,6 +114,12 @@ export type MaybeSignal = T | Signal; export type UnwrappedSignal = T extends Signal ? U : T; +export type LazyProperty = { + o: O; + p: P; + t: typeof LAZY_PROPERTY; +}; + export type EqualsFunctions = (a: T, v: T) => boolean; export type BlockType = diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 3cdfbeaf4a67..840e8cfd43e0 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -7,6 +7,7 @@ export { expose, exposable, source, + mutable_source, derived, prop, prop_source, @@ -38,12 +39,15 @@ export { reactive_import, effect_active, user_root_effect, - inspect + inspect, + unwrap } from './client/runtime.js'; -export * from './client/validate.js'; - +export * from './client/each.js'; export * from './client/render.js'; +export * from './client/validate.js'; +export { raf } from './client/timing.js'; +export { proxy, readonly } from './client/proxy/proxy.js'; export { create_custom_element } from './client/custom-element.js'; @@ -54,5 +58,3 @@ export { $window as window, $document as document } from './client/operations.js'; - -export { raf } from './client/timing.js'; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index dc0afe7ec208..562ee227012e 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -115,10 +115,9 @@ export function render(component, options) { /** * @param {boolean} runes - * @param {boolean} [immutable] */ -export function push(runes, immutable) { - $.push({}, runes, immutable); +export function push(runes) { + $.push({}, runes); } export function pop() { diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 8c63213208a2..9ad99401d879 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -68,7 +68,6 @@ class Svelte4Component { target: options.target, props: { ...options.props, $$events: this.#events }, context: options.context, - immutable: options.immutable, intro: options.intro, recover: options.recover }); diff --git a/packages/svelte/tests/compiler-errors/samples/export-derived-state/_config.js b/packages/svelte/tests/compiler-errors/samples/export-derived-state/_config.js new file mode 100644 index 000000000000..801d1d8a3077 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/export-derived-state/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-derived-export', + message: 'Cannot export derived state', + position: process.platform === 'win32' ? [26, 68] : [24, 66] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/export-derived-state/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/export-derived-state/main.svelte.js new file mode 100644 index 000000000000..0906cedf74ca --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/export-derived-state/main.svelte.js @@ -0,0 +1,3 @@ +let count = $state(0); + +export const double = $derived(count * 2); diff --git a/packages/svelte/tests/compiler-errors/samples/export-state/_config.js b/packages/svelte/tests/compiler-errors/samples/export-state/_config.js new file mode 100644 index 000000000000..755b891586f7 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/export-state/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-state-export', + message: 'Cannot export state if it is reassigned', + position: process.platform === 'win32' ? [50, 90] : [46, 86] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/export-state/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/export-state/main.svelte.js new file mode 100644 index 000000000000..39d9920f7da8 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/export-state/main.svelte.js @@ -0,0 +1,13 @@ +export const object = $state({ + ok: true +}); + +export const primitive = $state('nope'); + +export function update_object() { + object.ok = !object.ok; +} + +export function update_primitive() { + primitive = 'yep'; +} diff --git a/packages/svelte/tests/compiler-errors/test.ts b/packages/svelte/tests/compiler-errors/test.ts index b7a76c896fd2..376e790db651 100644 --- a/packages/svelte/tests/compiler-errors/test.ts +++ b/packages/svelte/tests/compiler-errors/test.ts @@ -37,11 +37,11 @@ const { test, run } = suite((config, cwd) => { } } - if (fs.existsSync(`${cwd}/main.js`)) { + if (fs.existsSync(`${cwd}/main.svelte.js`)) { let caught_error = false; try { - compileModule(fs.readFileSync(`${cwd}/main.js`, 'utf-8'), { + compileModule(fs.readFileSync(`${cwd}/main.svelte.js`, 'utf-8'), { generate: 'client' }); } catch (e) { @@ -51,6 +51,10 @@ const { test, run } = suite((config, cwd) => { expect(error.code).toMatch(config.error.code); expect(error.message).toMatch(config.error.message); + + if (config.error.position) { + expect(error.position).toEqual(config.error.position); + } } if (!caught_error) { diff --git a/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json b/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json index 99e9a0d15450..3dad9bb4e523 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-duplicate/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 35, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 35, - "type": "Element", "name": "input", "attributes": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json index f1d5293746a3..66ce187c625a 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-call/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 40, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 40, - "type": "Element", "name": "input", "attributes": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json index 4931f29ea98a..39a6f5f64702 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-identifier/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 29, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 29, - "type": "Element", "name": "input", "attributes": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json b/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json index e59e5e2d709c..94c60b701a44 100644 --- a/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action-with-literal/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 37, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 37, - "type": "Element", "name": "input", "attributes": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/action/output.json b/packages/svelte/tests/parser-legacy/samples/action/output.json index 0ed95d4ee80d..d72bf7db1012 100644 --- a/packages/svelte/tests/parser-legacy/samples/action/output.json +++ b/packages/svelte/tests/parser-legacy/samples/action/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 21, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 21, - "type": "Element", "name": "input", "attributes": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/animation/output.json b/packages/svelte/tests/parser-legacy/samples/animation/output.json index 3f967587f80b..6a43c13c2f42 100644 --- a/packages/svelte/tests/parser-legacy/samples/animation/output.json +++ b/packages/svelte/tests/parser-legacy/samples/animation/output.json @@ -1,34 +1,18 @@ { "html": { + "type": "Fragment", "start": 0, "end": 70, - "type": "Fragment", "children": [ { + "type": "EachBlock", "start": 0, "end": 70, - "type": "EachBlock", - "expression": { - "type": "Identifier", - "start": 7, - "end": 13, - "loc": { - "start": { - "line": 1, - "column": 7 - }, - "end": { - "line": 1, - "column": 13 - } - }, - "name": "things" - }, "children": [ { + "type": "Element", "start": 33, "end": 62, - "type": "Element", "name": "div", "attributes": [ { @@ -42,9 +26,9 @@ ], "children": [ { + "type": "Text", "start": 51, "end": 56, - "type": "Text", "raw": "flips", "data": "flips" } @@ -57,6 +41,22 @@ "start": 17, "end": 22 }, + "expression": { + "type": "Identifier", + "start": 7, + "end": 13, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 13 + } + }, + "name": "things" + }, "key": { "type": "Identifier", "start": 24, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json index 88708ebb6e75..9efe9acf8dda 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-class-directive/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 29, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 29, - "type": "Element", "name": "div", "attributes": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-containing-solidus/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-containing-solidus/output.json index a02fb12ae82b..2c63b3a43d13 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-containing-solidus/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-containing-solidus/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 41, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 41, - "type": "Element", "name": "a", "attributes": [ { + "type": "Attribute", "start": 3, "end": 30, - "type": "Attribute", "name": "href", "value": [ { @@ -28,9 +28,9 @@ ], "children": [ { + "type": "Text", "start": 31, "end": 37, - "type": "Text", "raw": "Google", "data": "Google" } diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-curly-bracket/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-curly-bracket/output.json index 077e307dff23..2453dc9e0a07 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-curly-bracket/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-curly-bracket/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 18, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 18, - "type": "Element", "name": "input", "attributes": [ { + "type": "Attribute", "start": 7, "end": 15, - "type": "Attribute", "name": "foo", "value": [ { @@ -24,9 +24,9 @@ "data": "a" }, { + "type": "MustacheTag", "start": 12, "end": 15, - "type": "MustacheTag", "expression": { "type": "Literal", "start": 13, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-dynamic-boolean/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-dynamic-boolean/output.json index b87a6546709e..5793afe896fd 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-dynamic-boolean/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-dynamic-boolean/output.json @@ -1,25 +1,25 @@ { "html": { + "type": "Fragment", "start": 0, "end": 41, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 41, - "type": "Element", "name": "textarea", "attributes": [ { + "type": "Attribute", "start": 10, "end": 29, - "type": "Attribute", "name": "readonly", "value": [ { + "type": "MustacheTag", "start": 19, "end": 29, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 20, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-dynamic/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-dynamic/output.json index 4926b645ab1b..9fd98c80ec27 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-dynamic/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-dynamic/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 42, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 42, - "type": "Element", "name": "div", "attributes": [ { + "type": "Attribute", "start": 5, "end": 28, - "type": "Attribute", "name": "style", "value": [ { @@ -24,9 +24,9 @@ "data": "color: " }, { + "type": "MustacheTag", "start": 19, "end": 26, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 20, @@ -56,9 +56,9 @@ ], "children": [ { + "type": "MustacheTag", "start": 29, "end": 36, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 30, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-empty/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-empty/output.json index 04b28f8d4a5e..d2a3dcd93bf6 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-empty/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-empty/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 38, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 38, - "type": "Element", "name": "div", "attributes": [ { + "type": "Attribute", "start": 5, "end": 9, - "type": "Attribute", "name": "a", "value": [ { @@ -26,15 +26,15 @@ ] }, { + "type": "Attribute", "start": 10, "end": 16, - "type": "Attribute", "name": "b", "value": [ { + "type": "MustacheTag", "start": 12, "end": 16, - "type": "MustacheTag", "expression": { "type": "Literal", "start": 13, @@ -56,9 +56,9 @@ ] }, { + "type": "Attribute", "start": 17, "end": 21, - "type": "Attribute", "name": "c", "value": [ { @@ -71,15 +71,15 @@ ] }, { + "type": "Attribute", "start": 22, "end": 30, - "type": "Attribute", "name": "d", "value": [ { + "type": "MustacheTag", "start": 25, "end": 29, - "type": "MustacheTag", "expression": { "type": "Literal", "start": 26, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-escaped/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-escaped/output.json index 71eb553a7116..e2eb99f32774 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-escaped/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-escaped/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 83, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 83, - "type": "Element", "name": "div", "attributes": [ { + "type": "Attribute", "start": 5, "end": 76, - "type": "Attribute", "name": "data-foo", "value": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-multiple/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-multiple/output.json index ce098fa537cb..66b780e53661 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-multiple/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-multiple/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 28, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 28, - "type": "Element", "name": "div", "attributes": [ { + "type": "Attribute", "start": 5, "end": 11, - "type": "Attribute", "name": "id", "value": [ { @@ -26,9 +26,9 @@ ] }, { + "type": "Attribute", "start": 12, "end": 21, - "type": "Attribute", "name": "class", "value": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-shorthand/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-shorthand/output.json index 4bfb7af187e8..5daf29018c3b 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-shorthand/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-shorthand/output.json @@ -1,25 +1,25 @@ { "html": { + "type": "Fragment", "start": 0, "end": 11, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 11, - "type": "Element", "name": "div", "attributes": [ { + "type": "Attribute", "start": 5, "end": 9, - "type": "Attribute", "name": "id", "value": [ { + "type": "AttributeShorthand", "start": 6, "end": 8, - "type": "AttributeShorthand", "expression": { "start": 6, "end": 8, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-static-boolean/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-static-boolean/output.json index cf65d545920d..8cb93b75ec08 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-static-boolean/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-static-boolean/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 30, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 30, - "type": "Element", "name": "textarea", "attributes": [ { + "type": "Attribute", "start": 10, "end": 18, - "type": "Attribute", "name": "readonly", "value": true } diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-static/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-static/output.json index b4fff2cd06d5..3e19a4727ea3 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-static/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-static/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 23, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 23, - "type": "Element", "name": "div", "attributes": [ { + "type": "Attribute", "start": 5, "end": 16, - "type": "Attribute", "name": "class", "value": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-modifiers/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-modifiers/output.json index 1587c2a3fb17..b7de71ff5ae6 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-modifiers/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-modifiers/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 43, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 43, - "type": "Element", "name": "div", "attributes": [ { @@ -18,9 +18,9 @@ "modifiers": ["important"], "value": [ { + "type": "MustacheTag", "start": 27, "end": 36, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 28, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-shorthand/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-shorthand/output.json index 80fb4f741fbf..d7f53cb00ca2 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-shorthand/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-shorthand/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 23, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 23, - "type": "Element", "name": "div", "attributes": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-string/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-string/output.json index 505e9333e134..5acf7d797eaa 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-string/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive-string/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 252, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 29, - "type": "Element", "name": "div", "attributes": [ { @@ -30,24 +30,24 @@ "children": [] }, { + "type": "Text", "start": 29, "end": 30, - "type": "Text", "raw": "\n", "data": "\n" }, { + "type": "Element", "start": 30, "end": 59, - "type": "Element", "name": "div", "attributes": [ { "start": 35, "end": 52, "type": "StyleDirective", - "modifiers": [], "name": "color", + "modifiers": [], "value": [ { "start": 48, @@ -62,16 +62,16 @@ "children": [] }, { + "type": "Text", "start": 59, "end": 60, - "type": "Text", "raw": "\n", "data": "\n" }, { + "type": "Element", "start": 60, "end": 87, - "type": "Element", "name": "div", "attributes": [ { @@ -94,16 +94,16 @@ "children": [] }, { + "type": "Text", "start": 87, "end": 88, - "type": "Text", "raw": "\n", "data": "\n" }, { + "type": "Element", "start": 88, "end": 127, - "type": "Element", "name": "div", "attributes": [ { @@ -121,9 +121,9 @@ "data": "red" }, { + "type": "MustacheTag", "start": 109, "end": 119, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 110, @@ -147,16 +147,16 @@ "children": [] }, { + "type": "Text", "start": 127, "end": 128, - "type": "Text", "raw": "\n", "data": "\n" }, { + "type": "Element", "start": 128, "end": 167, - "type": "Element", "name": "div", "attributes": [ { @@ -174,9 +174,9 @@ "data": "red" }, { + "type": "MustacheTag", "start": 149, "end": 159, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 150, @@ -200,16 +200,16 @@ "children": [] }, { + "type": "Text", "start": 167, "end": 168, - "type": "Text", "raw": "\n", "data": "\n" }, { + "type": "Element", "start": 168, "end": 205, - "type": "Element", "name": "div", "attributes": [ { @@ -227,9 +227,9 @@ "data": "red" }, { + "type": "MustacheTag", "start": 188, "end": 198, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 189, @@ -253,16 +253,16 @@ "children": [] }, { + "type": "Text", "start": 205, "end": 206, - "type": "Text", "raw": "\n", "data": "\n" }, { + "type": "Element", "start": 206, "end": 252, - "type": "Element", "name": "div", "attributes": [ { @@ -273,9 +273,9 @@ "modifiers": [], "value": [ { + "type": "MustacheTag", "start": 223, "end": 245, - "type": "MustacheTag", "expression": { "type": "TemplateLiteral", "start": 224, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive/output.json index b7983a769efb..2cce9fef952c 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-style-directive/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-style-directive/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 33, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 33, - "type": "Element", "name": "div", "attributes": [ { @@ -18,9 +18,9 @@ "modifiers": [], "value": [ { + "type": "MustacheTag", "start": 17, "end": 26, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 18, diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-style/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-style/output.json index 8dd604523d60..1d9a528d6d3d 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-style/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-style/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 34, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 34, - "type": "Element", "name": "div", "attributes": [ { + "type": "Attribute", "start": 5, "end": 24, - "type": "Attribute", "name": "style", "value": [ { @@ -28,9 +28,9 @@ ], "children": [ { + "type": "Text", "start": 25, "end": 28, - "type": "Text", "raw": "red", "data": "red" } diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json index 582ca0a2f312..5df4d66ab668 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 21, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 21, - "type": "Element", "name": "div", "attributes": [ { + "type": "Attribute", "start": 5, "end": 14, - "type": "Attribute", "name": "class", "value": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json index dbd1d8d60ac2..4d3a29180869 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-with-whitespace/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 38, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 38, - "type": "Element", "name": "button", "attributes": [ { @@ -36,9 +36,9 @@ ], "children": [ { + "type": "Text", "start": 24, "end": 29, - "type": "Text", "raw": "Click", "data": "Click" } diff --git a/packages/svelte/tests/parser-legacy/samples/await-catch/output.json b/packages/svelte/tests/parser-legacy/samples/await-catch/output.json index 163d061302e6..06a73d522cbe 100644 --- a/packages/svelte/tests/parser-legacy/samples/await-catch/output.json +++ b/packages/svelte/tests/parser-legacy/samples/await-catch/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 99, - "type": "Fragment", "children": [ { + "type": "AwaitBlock", "start": 0, "end": 99, - "type": "AwaitBlock", "expression": { "type": "Identifier", "start": 8, @@ -32,37 +32,37 @@ "end": 55 }, "pending": { + "type": "PendingBlock", "start": 19, "end": 39, - "type": "PendingBlock", "children": [ { + "type": "Text", "start": 19, "end": 21, - "type": "Text", "raw": "\n\t", "data": "\n\t" }, { + "type": "Element", "start": 21, "end": 38, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 24, "end": 34, - "type": "Text", "raw": "loading...", "data": "loading..." } ] }, { + "type": "Text", "start": 38, "end": 39, - "type": "Text", "raw": "\n", "data": "\n" } @@ -70,42 +70,42 @@ "skip": false }, "then": { + "type": "ThenBlock", "start": null, "end": null, - "type": "ThenBlock", "children": [], "skip": true }, "catch": { + "type": "CatchBlock", "start": 39, "end": 91, - "type": "CatchBlock", "children": [ { + "type": "Text", "start": 56, "end": 58, - "type": "Text", "raw": "\n\t", "data": "\n\t" }, { + "type": "Element", "start": 58, "end": 90, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 61, "end": 68, - "type": "Text", "raw": "oh no! ", "data": "oh no! " }, { + "type": "MustacheTag", "start": 68, "end": 86, - "type": "MustacheTag", "expression": { "type": "MemberExpression", "start": 69, @@ -159,9 +159,9 @@ ] }, { + "type": "Text", "start": 90, "end": 91, - "type": "Text", "raw": "\n", "data": "\n" } diff --git a/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json b/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json index 18b484b0c0c6..a2ccb995e018 100644 --- a/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json +++ b/packages/svelte/tests/parser-legacy/samples/await-then-catch/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 148, - "type": "Fragment", "children": [ { + "type": "AwaitBlock", "start": 0, "end": 148, - "type": "AwaitBlock", "expression": { "type": "Identifier", "start": 8, @@ -37,37 +37,37 @@ "end": 104 }, "pending": { + "type": "PendingBlock", "start": 19, "end": 39, - "type": "PendingBlock", "children": [ { + "type": "Text", "start": 19, "end": 21, - "type": "Text", "raw": "\n\t", "data": "\n\t" }, { + "type": "Element", "start": 21, "end": 38, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 24, "end": 34, - "type": "Text", "raw": "loading...", "data": "loading..." } ] }, { + "type": "Text", "start": 38, "end": 39, - "type": "Text", "raw": "\n", "data": "\n" } @@ -75,35 +75,35 @@ "skip": false }, "then": { + "type": "ThenBlock", "start": 39, "end": 88, - "type": "ThenBlock", "children": [ { + "type": "Text", "start": 55, "end": 57, - "type": "Text", "raw": "\n\t", "data": "\n\t" }, { + "type": "Element", "start": 57, "end": 87, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 60, "end": 73, - "type": "Text", "raw": "the value is ", "data": "the value is " }, { + "type": "MustacheTag", "start": 73, "end": 83, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 74, @@ -124,9 +124,9 @@ ] }, { + "type": "Text", "start": 87, "end": 88, - "type": "Text", "raw": "\n", "data": "\n" } @@ -134,35 +134,35 @@ "skip": false }, "catch": { + "type": "CatchBlock", "start": 88, "end": 140, - "type": "CatchBlock", "children": [ { + "type": "Text", "start": 105, "end": 107, - "type": "Text", "raw": "\n\t", "data": "\n\t" }, { + "type": "Element", "start": 107, "end": 139, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 110, "end": 117, - "type": "Text", "raw": "oh no! ", "data": "oh no! " }, { + "type": "MustacheTag", "start": 117, "end": 135, - "type": "MustacheTag", "expression": { "type": "MemberExpression", "start": 118, @@ -216,9 +216,9 @@ ] }, { + "type": "Text", "start": 139, "end": 140, - "type": "Text", "raw": "\n", "data": "\n" } diff --git a/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json b/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json index a86d3f635da3..672014629791 100644 --- a/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json +++ b/packages/svelte/tests/parser-legacy/samples/binding-shorthand/output.json @@ -1,20 +1,20 @@ { "html": { + "type": "Fragment", "start": 30, "end": 48, - "type": "Fragment", "children": [ { + "type": "Text", "start": 28, "end": 30, - "type": "Text", "raw": "\n\n", "data": "\n\n" }, { + "type": "InlineComponent", "start": 30, "end": 48, - "type": "InlineComponent", "name": "Widget", "attributes": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/binding/output.json b/packages/svelte/tests/parser-legacy/samples/binding/output.json index 3dad94d3da63..4ce069bd37c0 100644 --- a/packages/svelte/tests/parser-legacy/samples/binding/output.json +++ b/packages/svelte/tests/parser-legacy/samples/binding/output.json @@ -1,20 +1,20 @@ { "html": { + "type": "Fragment", "start": 31, "end": 56, - "type": "Fragment", "children": [ { + "type": "Text", "start": 29, "end": 31, - "type": "Text", "raw": "\n\n", "data": "\n\n" }, { + "type": "Element", "start": 31, "end": 56, - "type": "Element", "name": "input", "attributes": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/comment-with-ignores/output.json b/packages/svelte/tests/parser-legacy/samples/comment-with-ignores/output.json index e15150a09d1a..b149436a2702 100644 --- a/packages/svelte/tests/parser-legacy/samples/comment-with-ignores/output.json +++ b/packages/svelte/tests/parser-legacy/samples/comment-with-ignores/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 30, - "type": "Fragment", "children": [ { + "type": "Comment", "start": 0, "end": 30, - "type": "Comment", "data": " svelte-ignore foo bar ", "ignores": ["foo", "bar"] } diff --git a/packages/svelte/tests/parser-legacy/samples/comment/output.json b/packages/svelte/tests/parser-legacy/samples/comment/output.json index b3f050d650e2..6017db404cb9 100644 --- a/packages/svelte/tests/parser-legacy/samples/comment/output.json +++ b/packages/svelte/tests/parser-legacy/samples/comment/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 18, - "type": "Fragment", "children": [ { + "type": "Comment", "start": 0, "end": 18, - "type": "Comment", "data": " a comment ", "ignores": [] } diff --git a/packages/svelte/tests/parser-legacy/samples/component-dynamic/output.json b/packages/svelte/tests/parser-legacy/samples/component-dynamic/output.json index 06e17f80924b..f624a04c617b 100644 --- a/packages/svelte/tests/parser-legacy/samples/component-dynamic/output.json +++ b/packages/svelte/tests/parser-legacy/samples/component-dynamic/output.json @@ -1,16 +1,14 @@ { "html": { + "type": "Fragment", "start": 0, "end": 62, - "type": "Fragment", "children": [ { - "start": 0, - "end": 62, "type": "InlineComponent", "name": "svelte:component", - "attributes": [], - "children": [], + "start": 0, + "end": 62, "expression": { "type": "ConditionalExpression", "start": 25, @@ -73,7 +71,9 @@ }, "name": "Bar" } - } + }, + "attributes": [], + "children": [] } ] } diff --git a/packages/svelte/tests/parser-legacy/samples/convert-entities-in-element/output.json b/packages/svelte/tests/parser-legacy/samples/convert-entities-in-element/output.json index 15784cbcb65b..f7a8a3c67794 100644 --- a/packages/svelte/tests/parser-legacy/samples/convert-entities-in-element/output.json +++ b/packages/svelte/tests/parser-legacy/samples/convert-entities-in-element/output.json @@ -1,20 +1,20 @@ { "html": { + "type": "Fragment", "start": 0, "end": 24, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 24, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 3, "end": 20, - "type": "Text", "raw": "Hello & World", "data": "Hello & World" } diff --git a/packages/svelte/tests/parser-legacy/samples/convert-entities/output.json b/packages/svelte/tests/parser-legacy/samples/convert-entities/output.json index 5a3ad64c20f8..c336a3978c30 100644 --- a/packages/svelte/tests/parser-legacy/samples/convert-entities/output.json +++ b/packages/svelte/tests/parser-legacy/samples/convert-entities/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 17, - "type": "Fragment", "children": [ { + "type": "Text", "start": 0, "end": 17, - "type": "Text", "raw": "Hello & World", "data": "Hello & World" } diff --git a/packages/svelte/tests/parser-legacy/samples/css/output.json b/packages/svelte/tests/parser-legacy/samples/css/output.json index 31d3d145a95f..d1256eef459d 100644 --- a/packages/svelte/tests/parser-legacy/samples/css/output.json +++ b/packages/svelte/tests/parser-legacy/samples/css/output.json @@ -1,29 +1,29 @@ { "html": { + "type": "Fragment", "start": 0, "end": 14, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 14, - "type": "Element", "name": "div", "attributes": [], "children": [ { + "type": "Text", "start": 5, "end": 8, - "type": "Text", "raw": "foo", "data": "foo" } ] }, { + "type": "Text", "start": 14, "end": 16, - "type": "Text", "raw": "\n\n", "data": "\n\n" } @@ -39,9 +39,13 @@ "type": "Rule", "prelude": { "type": "SelectorList", + "start": 25, + "end": 28, "children": [ { "type": "Selector", + "start": 25, + "end": 28, "children": [ { "type": "TypeSelector", @@ -49,27 +53,23 @@ "start": 25, "end": 28 } - ], - "start": 25, - "end": 28 + ] } - ], - "start": 25, - "end": 28 + ] }, "block": { "type": "Block", + "start": 29, + "end": 47, "children": [ { "type": "Declaration", - "property": "color", - "value": "red", "start": 33, - "end": 43 + "end": 43, + "property": "color", + "value": "red" } - ], - "start": 29, - "end": 47 + ] }, "start": 25, "end": 47 diff --git a/packages/svelte/tests/parser-legacy/samples/dynamic-element-string/output.json b/packages/svelte/tests/parser-legacy/samples/dynamic-element-string/output.json index 9e9245b1aed7..6dd18f0a4eeb 100644 --- a/packages/svelte/tests/parser-legacy/samples/dynamic-element-string/output.json +++ b/packages/svelte/tests/parser-legacy/samples/dynamic-element-string/output.json @@ -1,30 +1,31 @@ { "html": { + "type": "Fragment", "start": 0, "end": 101, - "type": "Fragment", "children": [ { - "start": 0, - "end": 44, "type": "Element", "name": "svelte:element", + "start": 0, + "end": 44, + "tag": "div", "attributes": [], - "children": [], - "tag": "div" + "children": [] }, { "type": "Text", "start": 44, "end": 45, - "data": "\n", - "raw": "\n" + "raw": "\n", + "data": "\n" }, { - "start": 45, - "end": 101, "type": "Element", "name": "svelte:element", + "start": 45, + "end": 101, + "tag": "div", "attributes": [ { "type": "Attribute", @@ -33,17 +34,16 @@ "name": "class", "value": [ { - "type": "Text", "start": 79, "end": 82, - "data": "foo", - "raw": "foo" + "type": "Text", + "raw": "foo", + "data": "foo" } ] } ], - "children": [], - "tag": "div" + "children": [] } ] } diff --git a/packages/svelte/tests/parser-legacy/samples/dynamic-element-variable/output.json b/packages/svelte/tests/parser-legacy/samples/dynamic-element-variable/output.json index 53769e94e3b0..291cdaa73405 100644 --- a/packages/svelte/tests/parser-legacy/samples/dynamic-element-variable/output.json +++ b/packages/svelte/tests/parser-legacy/samples/dynamic-element-variable/output.json @@ -1,17 +1,16 @@ { "html": { + "type": "Fragment", "start": 0, "end": 101, - "type": "Fragment", "children": [ { - "start": 0, - "end": 44, "type": "Element", "name": "svelte:element", - "attributes": [], - "children": [], + "start": 0, + "end": 44, "tag": { + "type": "Identifier", "start": 22, "end": 25, "loc": { @@ -24,23 +23,39 @@ "column": 25 } }, - "name": "tag", - "type": "Identifier" - } + "name": "tag" + }, + "attributes": [], + "children": [] }, { "type": "Text", "start": 44, "end": 45, - "data": "\n", - "raw": "\n" + "raw": "\n", + "data": "\n" }, { - "start": 45, - "end": 101, "type": "Element", "name": "svelte:element", - "children": [], + "start": 45, + "end": 101, + "tag": { + "type": "Identifier", + "start": 67, + "end": 70, + "loc": { + "start": { + "line": 2, + "column": 22 + }, + "end": { + "line": 2, + "column": 25 + } + }, + "name": "tag" + }, "attributes": [ { "type": "Attribute", @@ -49,31 +64,16 @@ "name": "class", "value": [ { - "type": "Text", "start": 79, "end": 82, - "data": "foo", - "raw": "foo" + "type": "Text", + "raw": "foo", + "data": "foo" } ] } ], - "tag": { - "start": 67, - "end": 70, - "loc": { - "start": { - "line": 2, - "column": 22 - }, - "end": { - "line": 2, - "column": 25 - } - }, - "name": "tag", - "type": "Identifier" - } + "children": [] } ] } diff --git a/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json b/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json index 14ae36ae3ac9..a439b65dd0ec 100644 --- a/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json +++ b/packages/svelte/tests/parser-legacy/samples/dynamic-import/output.json @@ -1,8 +1,8 @@ { "html": { + "type": "Fragment", "start": null, "end": null, - "type": "Fragment", "children": [] }, "instance": { diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json index 9cbeb2be6c2e..d19f5cbbfd8c 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json @@ -1,48 +1,32 @@ { "html": { + "type": "Fragment", "start": 41, "end": 112, - "type": "Fragment", "children": [ { + "type": "Text", "start": 39, "end": 41, - "type": "Text", "raw": "\n\n", "data": "\n\n" }, { + "type": "EachBlock", "start": 41, "end": 112, - "type": "EachBlock", - "expression": { - "type": "Identifier", - "start": 48, - "end": 55, - "loc": { - "start": { - "line": 5, - "column": 7 - }, - "end": { - "line": 5, - "column": 14 - } - }, - "name": "animals" - }, "children": [ { + "type": "Element", "start": 83, "end": 104, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "MustacheTag", "start": 86, "end": 91, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 87, @@ -61,16 +45,16 @@ } }, { + "type": "Text", "start": 91, "end": 93, - "type": "Text", "raw": ": ", "data": ": " }, { + "type": "MustacheTag", "start": 93, "end": 100, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 94, @@ -170,6 +154,22 @@ } } ] + }, + "expression": { + "type": "Identifier", + "start": 48, + "end": 55, + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 14 + } + }, + "name": "animals" } } ] diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json index 935195985ffd..5af3bff86df3 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-else/output.json @@ -1,41 +1,25 @@ { "html": { + "type": "Fragment", "start": 0, "end": 77, - "type": "Fragment", "children": [ { + "type": "EachBlock", "start": 0, "end": 77, - "type": "EachBlock", - "expression": { - "type": "Identifier", - "start": 7, - "end": 14, - "loc": { - "start": { - "line": 1, - "column": 7 - }, - "end": { - "line": 1, - "column": 14 - } - }, - "name": "animals" - }, "children": [ { + "type": "Element", "start": 27, "end": 42, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "MustacheTag", "start": 30, "end": 38, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 31, @@ -62,22 +46,38 @@ "start": 18, "end": 24 }, + "expression": { + "type": "Identifier", + "start": 7, + "end": 14, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 14 + } + }, + "name": "animals" + }, "else": { + "type": "ElseBlock", "start": 50, "end": 70, - "type": "ElseBlock", "children": [ { + "type": "Element", "start": 52, "end": 69, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 55, "end": 65, - "type": "Text", "raw": "no animals", "data": "no animals" } diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json index ff5d5743269a..915dd6228b64 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-indexed/output.json @@ -1,41 +1,25 @@ { "html": { + "type": "Fragment", "start": 0, "end": 58, - "type": "Fragment", "children": [ { + "type": "EachBlock", "start": 0, "end": 58, - "type": "EachBlock", - "expression": { - "type": "Identifier", - "start": 7, - "end": 14, - "loc": { - "start": { - "line": 1, - "column": 7 - }, - "end": { - "line": 1, - "column": 14 - } - }, - "name": "animals" - }, "children": [ { + "type": "Element", "start": 30, "end": 50, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "MustacheTag", "start": 33, "end": 36, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 34, @@ -54,16 +38,16 @@ } }, { + "type": "Text", "start": 36, "end": 38, - "type": "Text", "raw": ": ", "data": ": " }, { + "type": "MustacheTag", "start": 38, "end": 46, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 39, @@ -90,6 +74,22 @@ "start": 18, "end": 24 }, + "expression": { + "type": "Identifier", + "start": 7, + "end": 14, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 14 + } + }, + "name": "animals" + }, "index": "i" } ] diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json index e4e96898517f..9582d4546564 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-keyed/output.json @@ -1,41 +1,25 @@ { "html": { + "type": "Fragment", "start": 0, "end": 54, - "type": "Fragment", "children": [ { + "type": "EachBlock", "start": 0, "end": 54, - "type": "EachBlock", - "expression": { - "type": "Identifier", - "start": 7, - "end": 12, - "loc": { - "start": { - "line": 1, - "column": 7 - }, - "end": { - "line": 1, - "column": 12 - } - }, - "name": "todos" - }, "children": [ { + "type": "Element", "start": 33, "end": 46, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "MustacheTag", "start": 36, "end": 42, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 37, @@ -62,6 +46,22 @@ "start": 16, "end": 20 }, + "expression": { + "type": "Identifier", + "start": 7, + "end": 12, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 12 + } + }, + "name": "todos" + }, "key": { "type": "MemberExpression", "start": 22, diff --git a/packages/svelte/tests/parser-legacy/samples/each-block/output.json b/packages/svelte/tests/parser-legacy/samples/each-block/output.json index 39373ea2a580..1b418dac5a67 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block/output.json @@ -1,41 +1,25 @@ { "html": { + "type": "Fragment", "start": 0, "end": 50, - "type": "Fragment", "children": [ { + "type": "EachBlock", "start": 0, "end": 50, - "type": "EachBlock", - "expression": { - "type": "Identifier", - "start": 7, - "end": 14, - "loc": { - "start": { - "line": 1, - "column": 7 - }, - "end": { - "line": 1, - "column": 14 - } - }, - "name": "animals" - }, "children": [ { + "type": "Element", "start": 27, "end": 42, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "MustacheTag", "start": 30, "end": 38, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 31, @@ -61,6 +45,22 @@ "name": "animal", "start": 18, "end": 24 + }, + "expression": { + "type": "Identifier", + "start": 7, + "end": 14, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 14 + } + }, + "name": "animals" } } ] diff --git a/packages/svelte/tests/parser-legacy/samples/element-with-attribute-empty-string/output.json b/packages/svelte/tests/parser-legacy/samples/element-with-attribute-empty-string/output.json index 8e8768eb62ee..7773256d44ea 100644 --- a/packages/svelte/tests/parser-legacy/samples/element-with-attribute-empty-string/output.json +++ b/packages/svelte/tests/parser-legacy/samples/element-with-attribute-empty-string/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 43, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 21, - "type": "Element", "name": "span", "attributes": [ { + "type": "Attribute", "start": 6, "end": 13, - "type": "Attribute", "name": "attr", "value": [ { @@ -29,22 +29,22 @@ "children": [] }, { + "type": "Text", "start": 21, "end": 22, - "type": "Text", "raw": "\n", "data": "\n" }, { + "type": "Element", "start": 22, "end": 43, - "type": "Element", "name": "span", "attributes": [ { + "type": "Attribute", "start": 28, "end": 35, - "type": "Attribute", "name": "attr", "value": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/element-with-attribute/output.json b/packages/svelte/tests/parser-legacy/samples/element-with-attribute/output.json index a80344101ebb..9477886bb289 100644 --- a/packages/svelte/tests/parser-legacy/samples/element-with-attribute/output.json +++ b/packages/svelte/tests/parser-legacy/samples/element-with-attribute/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 49, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 24, - "type": "Element", "name": "span", "attributes": [ { + "type": "Attribute", "start": 6, "end": 16, - "type": "Attribute", "name": "attr", "value": [ { @@ -29,22 +29,22 @@ "children": [] }, { + "type": "Text", "start": 24, "end": 25, - "type": "Text", "raw": "\n", "data": "\n" }, { + "type": "Element", "start": 25, "end": 49, - "type": "Element", "name": "span", "attributes": [ { + "type": "Attribute", "start": 31, "end": 41, - "type": "Attribute", "name": "attr", "value": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/element-with-mustache/output.json b/packages/svelte/tests/parser-legacy/samples/element-with-mustache/output.json index cb4ededf05b4..ce0dc25e85c6 100644 --- a/packages/svelte/tests/parser-legacy/samples/element-with-mustache/output.json +++ b/packages/svelte/tests/parser-legacy/samples/element-with-mustache/output.json @@ -1,27 +1,27 @@ { "html": { + "type": "Fragment", "start": 0, "end": 22, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 22, - "type": "Element", "name": "h1", "attributes": [], "children": [ { + "type": "Text", "start": 4, "end": 10, - "type": "Text", "raw": "hello ", "data": "hello " }, { + "type": "MustacheTag", "start": 10, "end": 16, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 11, @@ -40,9 +40,9 @@ } }, { + "type": "Text", "start": 16, "end": 17, - "type": "Text", "raw": "!", "data": "!" } diff --git a/packages/svelte/tests/parser-legacy/samples/element-with-text/output.json b/packages/svelte/tests/parser-legacy/samples/element-with-text/output.json index 0106e75b4512..fde57c470a64 100644 --- a/packages/svelte/tests/parser-legacy/samples/element-with-text/output.json +++ b/packages/svelte/tests/parser-legacy/samples/element-with-text/output.json @@ -1,20 +1,20 @@ { "html": { + "type": "Fragment", "start": 0, "end": 17, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 17, - "type": "Element", "name": "span", "attributes": [], "children": [ { + "type": "Text", "start": 6, "end": 10, - "type": "Text", "raw": "test", "data": "test" } diff --git a/packages/svelte/tests/parser-legacy/samples/elements/output.json b/packages/svelte/tests/parser-legacy/samples/elements/output.json index c530d4376c50..6b51383d9339 100644 --- a/packages/svelte/tests/parser-legacy/samples/elements/output.json +++ b/packages/svelte/tests/parser-legacy/samples/elements/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 15, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 15, - "type": "Element", "name": "!doctype", "attributes": [ { + "type": "Attribute", "start": 10, "end": 14, - "type": "Attribute", "name": "html", "value": true } diff --git a/packages/svelte/tests/parser-legacy/samples/event-handler/output.json b/packages/svelte/tests/parser-legacy/samples/event-handler/output.json index e19804f20f24..45b625667706 100644 --- a/packages/svelte/tests/parser-legacy/samples/event-handler/output.json +++ b/packages/svelte/tests/parser-legacy/samples/event-handler/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 97, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 61, - "type": "Element", "name": "button", "attributes": [ { @@ -105,25 +105,25 @@ ], "children": [ { + "type": "Text", "start": 46, "end": 52, - "type": "Text", "raw": "toggle", "data": "toggle" } ] }, { + "type": "Text", "start": 61, "end": 63, - "type": "Text", "raw": "\n\n", "data": "\n\n" }, { + "type": "IfBlock", "start": 63, "end": 97, - "type": "IfBlock", "expression": { "type": "Identifier", "start": 68, @@ -142,16 +142,16 @@ }, "children": [ { + "type": "Element", "start": 78, "end": 91, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 81, "end": 87, - "type": "Text", "raw": "hello!", "data": "hello!" } diff --git a/packages/svelte/tests/parser-legacy/samples/if-block-else/output.json b/packages/svelte/tests/parser-legacy/samples/if-block-else/output.json index 642768558323..4ba9370d8870 100644 --- a/packages/svelte/tests/parser-legacy/samples/if-block-else/output.json +++ b/packages/svelte/tests/parser-legacy/samples/if-block-else/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 51, - "type": "Fragment", "children": [ { + "type": "IfBlock", "start": 0, "end": 51, - "type": "IfBlock", "expression": { "type": "Identifier", "start": 5, @@ -26,16 +26,16 @@ }, "children": [ { + "type": "Element", "start": 11, "end": 21, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 14, "end": 17, - "type": "Text", "raw": "foo", "data": "foo" } @@ -43,21 +43,21 @@ } ], "else": { + "type": "ElseBlock", "start": 29, "end": 46, - "type": "ElseBlock", "children": [ { + "type": "Element", "start": 31, "end": 45, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 34, "end": 41, - "type": "Text", "raw": "not foo", "data": "not foo" } diff --git a/packages/svelte/tests/parser-legacy/samples/if-block-elseif/output.json b/packages/svelte/tests/parser-legacy/samples/if-block-elseif/output.json index 2fdeddcf8c8f..3e6953ab0b3d 100644 --- a/packages/svelte/tests/parser-legacy/samples/if-block-elseif/output.json +++ b/packages/svelte/tests/parser-legacy/samples/if-block-elseif/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 89, - "type": "Fragment", "children": [ { + "type": "IfBlock", "start": 0, "end": 89, - "type": "IfBlock", "expression": { "type": "BinaryExpression", "start": 5, @@ -59,16 +59,16 @@ }, "children": [ { + "type": "Element", "start": 14, "end": 41, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 17, "end": 37, - "type": "Text", "raw": "x is greater than 10", "data": "x is greater than 10" } @@ -76,15 +76,14 @@ } ], "else": { + "type": "ElseBlock", "start": 58, "end": 84, - "type": "ElseBlock", "children": [ { + "type": "IfBlock", "start": 58, "end": 89, - "type": "IfBlock", - "elseif": true, "expression": { "type": "BinaryExpression", "start": 52, @@ -136,22 +135,23 @@ }, "children": [ { + "type": "Element", "start": 60, "end": 83, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 63, "end": 79, - "type": "Text", "raw": "x is less than 5", "data": "x is less than 5" } ] } - ] + ], + "elseif": true } ] } diff --git a/packages/svelte/tests/parser-legacy/samples/if-block/output.json b/packages/svelte/tests/parser-legacy/samples/if-block/output.json index 4c6fd97520b0..61ecf6e3b2b4 100644 --- a/packages/svelte/tests/parser-legacy/samples/if-block/output.json +++ b/packages/svelte/tests/parser-legacy/samples/if-block/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 17, - "type": "Fragment", "children": [ { + "type": "IfBlock", "start": 0, "end": 17, - "type": "IfBlock", "expression": { "type": "Identifier", "start": 5, @@ -26,9 +26,9 @@ }, "children": [ { + "type": "Text", "start": 9, "end": 12, - "type": "Text", "raw": "bar", "data": "bar" } diff --git a/packages/svelte/tests/parser-legacy/samples/implicitly-closed-li/output.json b/packages/svelte/tests/parser-legacy/samples/implicitly-closed-li/output.json index d46de69dd6cb..a56701894e63 100644 --- a/packages/svelte/tests/parser-legacy/samples/implicitly-closed-li/output.json +++ b/packages/svelte/tests/parser-legacy/samples/implicitly-closed-li/output.json @@ -1,66 +1,66 @@ { "html": { + "type": "Fragment", "start": 0, "end": 31, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 31, - "type": "Element", "name": "ul", "attributes": [], "children": [ { + "type": "Text", "start": 4, "end": 6, - "type": "Text", "raw": "\n\t", "data": "\n\t" }, { + "type": "Element", "start": 6, "end": 13, - "type": "Element", "name": "li", "attributes": [], "children": [ { + "type": "Text", "start": 10, "end": 13, - "type": "Text", "raw": "a\n\t", "data": "a\n\t" } ] }, { + "type": "Element", "start": 13, "end": 20, - "type": "Element", "name": "li", "attributes": [], "children": [ { + "type": "Text", "start": 17, "end": 20, - "type": "Text", "raw": "b\n\t", "data": "b\n\t" } ] }, { + "type": "Element", "start": 20, "end": 26, - "type": "Element", "name": "li", "attributes": [], "children": [ { + "type": "Text", "start": 24, "end": 26, - "type": "Text", "raw": "c\n", "data": "c\n" } diff --git a/packages/svelte/tests/parser-legacy/samples/nbsp/output.json b/packages/svelte/tests/parser-legacy/samples/nbsp/output.json index ec231bbe73d5..7d2158955a64 100644 --- a/packages/svelte/tests/parser-legacy/samples/nbsp/output.json +++ b/packages/svelte/tests/parser-legacy/samples/nbsp/output.json @@ -1,20 +1,20 @@ { "html": { + "type": "Fragment", "start": 0, "end": 19, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 19, - "type": "Element", "name": "span", "attributes": [], "children": [ { + "type": "Text", "start": 6, "end": 12, - "type": "Text", "raw": " ", "data": " " } diff --git a/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json b/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json index 4350d3cb5ba8..79a26426ded9 100644 --- a/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json +++ b/packages/svelte/tests/parser-legacy/samples/no-error-if-before-closing/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 148, - "type": "Fragment", "children": [ { + "type": "IfBlock", "start": 0, "end": 33, - "type": "IfBlock", "expression": { "type": "Literal", "start": 5, @@ -27,32 +27,32 @@ }, "children": [ { + "type": "Element", "start": 12, "end": 19, - "type": "Element", "name": "input", "attributes": [], "children": [] } ], "else": { + "type": "ElseBlock", "start": 27, "end": 28, - "type": "ElseBlock", "children": [] } }, { + "type": "Text", "start": 33, "end": 35, - "type": "Text", "raw": "\n\n", "data": "\n\n" }, { + "type": "IfBlock", "start": 35, "end": 65, - "type": "IfBlock", "expression": { "type": "Literal", "start": 40, @@ -72,32 +72,32 @@ }, "children": [ { + "type": "Element", "start": 47, "end": 51, - "type": "Element", "name": "br", "attributes": [], "children": [] } ], "else": { + "type": "ElseBlock", "start": 59, "end": 60, - "type": "ElseBlock", "children": [] } }, { + "type": "Text", "start": 65, "end": 67, - "type": "Text", "raw": "\n\n", "data": "\n\n" }, { + "type": "AwaitBlock", "start": 67, "end": 108, - "type": "AwaitBlock", "expression": { "type": "Literal", "start": 75, @@ -123,29 +123,29 @@ }, "error": null, "pending": { + "type": "PendingBlock", "start": 80, "end": 90, - "type": "PendingBlock", "children": [ { + "type": "Text", "start": 80, "end": 82, - "type": "Text", "raw": "\n\t", "data": "\n\t" }, { + "type": "Element", "start": 82, "end": 89, - "type": "Element", "name": "input", "attributes": [], "children": [] }, { + "type": "Text", "start": 89, "end": 90, - "type": "Text", "raw": "\n", "data": "\n" } @@ -153,14 +153,14 @@ "skip": false }, "then": { + "type": "ThenBlock", "start": 90, "end": 100, - "type": "ThenBlock", "children": [ { + "type": "Text", "start": 99, "end": 100, - "type": "Text", "raw": "\n", "data": "\n" } @@ -168,24 +168,24 @@ "skip": false }, "catch": { + "type": "CatchBlock", "start": null, "end": null, - "type": "CatchBlock", "children": [], "skip": true } }, { + "type": "Text", "start": 108, "end": 110, - "type": "Text", "raw": "\n\n", "data": "\n\n" }, { + "type": "AwaitBlock", "start": 110, "end": 148, - "type": "AwaitBlock", "expression": { "type": "Literal", "start": 118, @@ -211,29 +211,29 @@ }, "error": null, "pending": { + "type": "PendingBlock", "start": 123, "end": 130, - "type": "PendingBlock", "children": [ { + "type": "Text", "start": 123, "end": 125, - "type": "Text", "raw": "\n\t", "data": "\n\t" }, { + "type": "Element", "start": 125, "end": 129, - "type": "Element", "name": "br", "attributes": [], "children": [] }, { + "type": "Text", "start": 129, "end": 130, - "type": "Text", "raw": "\n", "data": "\n" } @@ -241,14 +241,14 @@ "skip": false }, "then": { + "type": "ThenBlock", "start": 130, "end": 140, - "type": "ThenBlock", "children": [ { + "type": "Text", "start": 139, "end": 140, - "type": "Text", "raw": "\n", "data": "\n" } @@ -256,9 +256,9 @@ "skip": false }, "catch": { + "type": "CatchBlock", "start": null, "end": null, - "type": "CatchBlock", "children": [], "skip": true } diff --git a/packages/svelte/tests/parser-legacy/samples/raw-mustaches/output.json b/packages/svelte/tests/parser-legacy/samples/raw-mustaches/output.json index 3c349b472f71..7db0bb7ec15a 100644 --- a/packages/svelte/tests/parser-legacy/samples/raw-mustaches/output.json +++ b/packages/svelte/tests/parser-legacy/samples/raw-mustaches/output.json @@ -1,27 +1,27 @@ { "html": { + "type": "Fragment", "start": 0, "end": 34, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 34, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 3, "end": 4, - "type": "Text", "raw": " ", "data": " " }, { + "type": "RawMustacheTag", "start": 4, "end": 16, - "type": "RawMustacheTag", "expression": { "type": "Identifier", "start": 11, @@ -40,16 +40,16 @@ } }, { + "type": "Text", "start": 16, "end": 17, - "type": "Text", "raw": " ", "data": " " }, { + "type": "RawMustacheTag", "start": 17, "end": 29, - "type": "RawMustacheTag", "expression": { "type": "Identifier", "start": 24, @@ -68,9 +68,9 @@ } }, { + "type": "Text", "start": 29, "end": 30, - "type": "Text", "raw": " ", "data": " " } diff --git a/packages/svelte/tests/parser-legacy/samples/refs/output.json b/packages/svelte/tests/parser-legacy/samples/refs/output.json index 75f5d2d52abc..e2bda741fa71 100644 --- a/packages/svelte/tests/parser-legacy/samples/refs/output.json +++ b/packages/svelte/tests/parser-legacy/samples/refs/output.json @@ -1,20 +1,20 @@ { "html": { + "type": "Fragment", "start": 30, "end": 63, - "type": "Fragment", "children": [ { + "type": "Text", "start": 28, "end": 30, - "type": "Text", "raw": "\n\n", "data": "\n\n" }, { + "type": "Element", "start": 30, "end": 63, - "type": "Element", "name": "canvas", "attributes": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json b/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json index af4ec56a68d2..e0b50e3b92db 100644 --- a/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json +++ b/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json @@ -1,34 +1,34 @@ { "html": { + "type": "Fragment", "start": 79, "end": 101, - "type": "Fragment", "children": [ { + "type": "Text", "start": 77, "end": 79, - "type": "Text", "raw": "\n\n", "data": "\n\n" }, { + "type": "Element", "start": 79, "end": 101, - "type": "Element", "name": "h1", "attributes": [], "children": [ { + "type": "Text", "start": 83, "end": 89, - "type": "Text", "raw": "Hello ", "data": "Hello " }, { + "type": "MustacheTag", "start": 89, "end": 95, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 90, @@ -47,9 +47,9 @@ } }, { + "type": "Text", "start": 95, "end": 96, - "type": "Text", "raw": "!", "data": "!" } diff --git a/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json b/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json index 072e10178289..03a526f04e37 100644 --- a/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json +++ b/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 62, "end": 60, - "type": "Fragment", "children": [ { + "type": "Text", "start": 60, "end": 62, - "type": "Text", "raw": "\n\n", "data": "\n\n" } diff --git a/packages/svelte/tests/parser-legacy/samples/script/output.json b/packages/svelte/tests/parser-legacy/samples/script/output.json index efa31bddb839..d3d4abd6b24f 100644 --- a/packages/svelte/tests/parser-legacy/samples/script/output.json +++ b/packages/svelte/tests/parser-legacy/samples/script/output.json @@ -1,34 +1,34 @@ { "html": { + "type": "Fragment", "start": 41, "end": 63, - "type": "Fragment", "children": [ { + "type": "Text", "start": 39, "end": 41, - "type": "Text", "raw": "\n\n", "data": "\n\n" }, { + "type": "Element", "start": 41, "end": 63, - "type": "Element", "name": "h1", "attributes": [], "children": [ { + "type": "Text", "start": 45, "end": 51, - "type": "Text", "raw": "Hello ", "data": "Hello " }, { + "type": "MustacheTag", "start": 51, "end": 57, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 52, @@ -47,9 +47,9 @@ } }, { + "type": "Text", "start": 57, "end": 58, - "type": "Text", "raw": "!", "data": "!" } diff --git a/packages/svelte/tests/parser-legacy/samples/self-closing-element/output.json b/packages/svelte/tests/parser-legacy/samples/self-closing-element/output.json index 34052615d7bb..f0ba3a5c0d7a 100644 --- a/packages/svelte/tests/parser-legacy/samples/self-closing-element/output.json +++ b/packages/svelte/tests/parser-legacy/samples/self-closing-element/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 6, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 6, - "type": "Element", "name": "div", "attributes": [], "children": [] diff --git a/packages/svelte/tests/parser-legacy/samples/self-reference/output.json b/packages/svelte/tests/parser-legacy/samples/self-reference/output.json index 77bffaaa3d8b..34310fcce46c 100644 --- a/packages/svelte/tests/parser-legacy/samples/self-reference/output.json +++ b/packages/svelte/tests/parser-legacy/samples/self-reference/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 57, - "type": "Fragment", "children": [ { + "type": "IfBlock", "start": 0, "end": 57, - "type": "IfBlock", "expression": { "type": "BinaryExpression", "start": 5, @@ -59,21 +59,21 @@ }, "children": [ { - "start": 17, - "end": 51, "type": "InlineComponent", "name": "svelte:self", + "start": 17, + "end": 51, "attributes": [ { + "type": "Attribute", "start": 30, "end": 49, - "type": "Attribute", "name": "depth", "value": [ { + "type": "MustacheTag", "start": 37, "end": 48, - "type": "MustacheTag", "expression": { "type": "BinaryExpression", "start": 38, diff --git a/packages/svelte/tests/parser-legacy/samples/slotted-element/output.json b/packages/svelte/tests/parser-legacy/samples/slotted-element/output.json index 7028a367ff80..90ded681031c 100644 --- a/packages/svelte/tests/parser-legacy/samples/slotted-element/output.json +++ b/packages/svelte/tests/parser-legacy/samples/slotted-element/output.json @@ -1,26 +1,26 @@ { "html": { + "type": "Fragment", "start": 0, "end": 45, - "type": "Fragment", "children": [ { + "type": "InlineComponent", "start": 0, "end": 45, - "type": "InlineComponent", "name": "Component", "attributes": [], "children": [ { + "type": "Element", "start": 11, "end": 33, - "type": "Element", "name": "div", "attributes": [ { + "type": "Attribute", "start": 16, "end": 26, - "type": "Attribute", "name": "slot", "value": [ { diff --git a/packages/svelte/tests/parser-legacy/samples/space-between-mustaches/output.json b/packages/svelte/tests/parser-legacy/samples/space-between-mustaches/output.json index c552a84b028a..bb6de0bea951 100644 --- a/packages/svelte/tests/parser-legacy/samples/space-between-mustaches/output.json +++ b/packages/svelte/tests/parser-legacy/samples/space-between-mustaches/output.json @@ -1,27 +1,27 @@ { "html": { + "type": "Fragment", "start": 0, "end": 24, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 24, - "type": "Element", "name": "p", "attributes": [], "children": [ { + "type": "Text", "start": 3, "end": 4, - "type": "Text", "raw": " ", "data": " " }, { + "type": "MustacheTag", "start": 4, "end": 7, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 5, @@ -40,16 +40,16 @@ } }, { + "type": "Text", "start": 7, "end": 8, - "type": "Text", "raw": " ", "data": " " }, { + "type": "MustacheTag", "start": 8, "end": 11, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 9, @@ -68,16 +68,16 @@ } }, { + "type": "Text", "start": 11, "end": 14, - "type": "Text", "raw": " : ", "data": " : " }, { + "type": "MustacheTag", "start": 14, "end": 17, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 15, @@ -96,9 +96,9 @@ } }, { + "type": "Text", "start": 17, "end": 20, - "type": "Text", "raw": " : ", "data": " : " } diff --git a/packages/svelte/tests/parser-legacy/samples/spread/output.json b/packages/svelte/tests/parser-legacy/samples/spread/output.json index e0a45f9d7587..3b79aa967028 100644 --- a/packages/svelte/tests/parser-legacy/samples/spread/output.json +++ b/packages/svelte/tests/parser-legacy/samples/spread/output.json @@ -1,19 +1,19 @@ { "html": { + "type": "Fragment", "start": 0, "end": 22, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 22, - "type": "Element", "name": "div", "attributes": [ { + "type": "Spread", "start": 5, "end": 15, - "type": "Spread", "expression": { "type": "Identifier", "start": 9, diff --git a/packages/svelte/tests/parser-legacy/samples/style-inside-head/output.json b/packages/svelte/tests/parser-legacy/samples/style-inside-head/output.json index 5082c5534334..733f9e76fff4 100644 --- a/packages/svelte/tests/parser-legacy/samples/style-inside-head/output.json +++ b/packages/svelte/tests/parser-legacy/samples/style-inside-head/output.json @@ -1,27 +1,27 @@ { "html": { + "type": "Fragment", "start": 0, "end": 42, - "type": "Fragment", "children": [ { - "start": 0, - "end": 42, "type": "Head", "name": "svelte:head", + "start": 0, + "end": 42, "attributes": [], "children": [ { + "type": "Element", "start": 13, "end": 28, - "type": "Element", "name": "style", "attributes": [], "children": [ { + "type": "Text", "start": 20, "end": 20, - "type": "Text", "data": "" } ] diff --git a/packages/svelte/tests/parser-legacy/samples/textarea-children/output.json b/packages/svelte/tests/parser-legacy/samples/textarea-children/output.json index cdb4b885d16a..60477807b79c 100644 --- a/packages/svelte/tests/parser-legacy/samples/textarea-children/output.json +++ b/packages/svelte/tests/parser-legacy/samples/textarea-children/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 61, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 61, - "type": "Element", "name": "textarea", "attributes": [], "children": [ @@ -19,9 +19,9 @@ "data": "\n\t

not actually an element. " }, { + "type": "MustacheTag", "start": 40, "end": 45, - "type": "MustacheTag", "expression": { "type": "Identifier", "start": 41, diff --git a/packages/svelte/tests/parser-legacy/samples/textarea-end-tag/output.json b/packages/svelte/tests/parser-legacy/samples/textarea-end-tag/output.json index 9693db272953..ed3c85b09a07 100644 --- a/packages/svelte/tests/parser-legacy/samples/textarea-end-tag/output.json +++ b/packages/svelte/tests/parser-legacy/samples/textarea-end-tag/output.json @@ -1,13 +1,13 @@ { "html": { + "type": "Fragment", "start": 0, "end": 117, - "type": "Fragment", "children": [ { + "type": "Element", "start": 0, "end": 117, - "type": "Element", "name": "textarea", "attributes": [], "children": [ @@ -19,9 +19,9 @@ "data": "\n\t

not actu

{item.description}

{/each} -

{numCompleted} completed

\ No newline at end of file +

{numCompleted} completed

diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 6ffffe33ef77..a1944d47e5f3 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -173,6 +173,11 @@ async function run_test_variant( window.document.head.innerHTML = styles ? `` : ''; window.document.body.innerHTML = '
'; + window.addEventListener('error', (e) => { + unhandled_rejection = e.error; + e.preventDefault(); + }); + let mod = await import(`${cwd}/_output/client/main.svelte.js`); const target = window.document.querySelector('main') as HTMLElement; diff --git a/packages/svelte/tests/runtime-runes/samples/each-mutation/_config.js b/packages/svelte/tests/runtime-runes/samples/each-mutation/_config.js index 399f29a43572..0dac8c623b08 100644 --- a/packages/svelte/tests/runtime-runes/samples/each-mutation/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/each-mutation/_config.js @@ -4,7 +4,7 @@ export default test({ html: ` - + `, async test({ assert, target }) { @@ -25,7 +25,7 @@ export default test({ ` - + ` ); } diff --git a/packages/svelte/tests/runtime-runes/samples/each-mutation/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-mutation/main.svelte index 5bf92b274bfb..6e0223d93b9c 100644 --- a/packages/svelte/tests/runtime-runes/samples/each-mutation/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/each-mutation/main.svelte @@ -1,20 +1,20 @@ -{#each works1 as item, i} - +{#each a as item, i} + {/each} -{#each works2 as item} +{#each b as item} {/each} -{#each doesntwork as item} +{#each c as item} {/each} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order/main.svelte index b83c73b95806..6f1967242502 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-order/main.svelte @@ -1,21 +1,20 @@ -

Hello {myList[0].age}

+

{s}

- + diff --git a/packages/svelte/tests/runtime-runes/samples/nullish-operator/_config.js b/packages/svelte/tests/runtime-runes/samples/nullish-operator/_config.js index 90c7fd7f3292..4c425bc5f7f6 100644 --- a/packages/svelte/tests/runtime-runes/samples/nullish-operator/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/nullish-operator/_config.js @@ -8,6 +8,6 @@ export default test({ async test({ assert, target, component }) { await Promise.resolve(); await Promise.resolve(); - assert.deepEqual(component.log, ['a1: ', {}, 'b1: ', {}]); + assert.deepEqual(component.log, ['a1: ', true, 'b1: ', true]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/nullish-operator/main.svelte b/packages/svelte/tests/runtime-runes/samples/nullish-operator/main.svelte index 8eb78927c1d6..892973063a38 100644 --- a/packages/svelte/tests/runtime-runes/samples/nullish-operator/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/nullish-operator/main.svelte @@ -3,26 +3,26 @@ let b1 = $state(); const {log} = $props(); - + $effect(() => { log.push('a1: ', a1); }); $effect(() => { log.push('b1: ', b1); }); - + a(); queueMicrotask(a); - + b(); queueMicrotask(b); - + function a() { - a1 ??= {}; + a1 ??= true; } - + function b() { - b1 ?? (b1 = {}); + b1 ?? (b1 = true); } diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-array/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-array/_config.js new file mode 100644 index 000000000000..e878dad88fc5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-array/_config.js @@ -0,0 +1,66 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + + 1 + 2 + 3 + array[1]: 2 + `, + + async test({ assert, target }) { + const [add, clear, reverse] = target.querySelectorAll('button'); + + await add?.click(); + assert.htmlEqual( + target.innerHTML, + ` + + + + 1 + 2 + 3 + 4 + array[1]: 2 + ` + ); + + flushSync(() => { + reverse?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + ` + + + + 4 + 3 + 2 + 1 + array[1]: 3 + ` + ); + + flushSync(() => { + clear?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + ` + + + + 4 + array[1]: + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-array/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-array/main.svelte new file mode 100644 index 000000000000..f2a9f5cfcc0f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-array/main.svelte @@ -0,0 +1,22 @@ + + + + + + + + +{#each array as n} + {n} +{/each} + +array[1]: {array[1]} diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-cyclical/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-cyclical/_config.js new file mode 100644 index 000000000000..185a198ca66f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-cyclical/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + await btn?.click(); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-cyclical/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-cyclical/main.svelte new file mode 100644 index 000000000000..f15777296352 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-cyclical/main.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-deep/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-deep/_config.js new file mode 100644 index 000000000000..25a71728532a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-deep/_config.js @@ -0,0 +1,39 @@ +import { test } from '../../test'; + +export default test({ + html: ` + + + `, + + async test({ assert, target }) { + const [btn1, btn2] = target.querySelectorAll('button'); + + await btn1?.click(); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + await btn2?.click(); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + await btn1?.click(); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-deep/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-deep/main.svelte new file mode 100644 index 000000000000..0acba3173d38 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-deep/main.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-nested/_config.js new file mode 100644 index 000000000000..fdf60d91fd1f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-nested/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + await btn?.click(); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-nested/main.svelte new file mode 100644 index 000000000000..a0f78ba3bb87 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-nested/main.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-bound/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-bound/Counter.svelte new file mode 100644 index 000000000000..d1be32683013 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-prop-bound/Counter.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-bound/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-prop-bound/_config.js new file mode 100644 index 000000000000..bdab6c9e39fd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-prop-bound/_config.js @@ -0,0 +1,21 @@ +import { test } from '../../test'; + +export default test({ + html: ` + +

object.count: 0

+ `, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + await btn?.click(); + + assert.htmlEqual( + target.innerHTML, + ` + +

object.count: 1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-bound/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-bound/main.svelte new file mode 100644 index 000000000000..d9d6d7ec306e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-prop-bound/main.svelte @@ -0,0 +1,9 @@ + + + + +

object.count: {object.count}

diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/Counter.svelte new file mode 100644 index 000000000000..d1be32683013 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/Counter.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js new file mode 100644 index 000000000000..5e0eb0e2da60 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js @@ -0,0 +1,18 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + compileOptions: { + dev: true + }, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + await btn?.click(); + + assert.htmlEqual(target.innerHTML, ``); + }, + + runtime_error: 'Props are read-only, unless used with `bind:`' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/main.svelte new file mode 100644 index 000000000000..e9617af17d5c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/main.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/proxy/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy/_config.js new file mode 100644 index 000000000000..fdf60d91fd1f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + await btn?.click(); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy/main.svelte new file mode 100644 index 000000000000..b83d9ec2b0b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy/main.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index ab886d5651b0..8f8f96c12e44 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -112,10 +112,7 @@ describe('signals', () => { const A = $.source(0); const B = $.source(0); const C = $.derived(() => ($.get(A) % 2) + ($.get(B) % 2)); - const D = $.derived( - () => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2)), - (l: number[], r: number[]) => l.length === r.length && l.every((v, i) => v === r[i]) - ); + const D = $.derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2))); const E = $.derived(() => hard($.get(C) + $.get(A) + $.get(D)[0]!, 'E')); const F = $.derived(() => hard($.get(D)[0]! && $.get(B), 'F')); const G = $.derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[0]! + $.get(F)); diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js index 836fd0ed0827..4add64c66fca 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js @@ -26,4 +26,4 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro } $.pop(); -} +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index 6f53521673fb..713b4e13376d 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -9,8 +9,8 @@ export default function Main($$anchor, $$props) { $.push($$props, true); // needs to be a snapshot test because jsdom does auto-correct the attribute casing - let x = $.source('test'); - let y = $.source(() => 'test'); + let x = 'test'; + let y = () => 'test'; /* Init */ var fragment = $.open_frag($$anchor, false, frag); var node = $.child_frag(fragment); @@ -21,28 +21,28 @@ export default function Main($$anchor, $$props) { var custom_element_1 = $.sibling($.sibling(svg_1)); /* Update */ - $.attr_effect(div, "foobar", () => $.get(y)()); - $.attr_effect(svg_1, "viewBox", () => $.get(y)()); - $.set_custom_element_data_effect(custom_element_1, "fooBar", () => $.get(y)()); + $.attr_effect(div, "foobar", () => y()); + $.attr_effect(svg_1, "viewBox", () => y()); + $.set_custom_element_data_effect(custom_element_1, "fooBar", () => y()); var node_foobar; var svg_viewBox; var custom_element_fooBar; $.render_effect(() => { - if (node_foobar !== (node_foobar = $.get(x))) { + if (node_foobar !== (node_foobar = x)) { $.attr(node, "foobar", node_foobar); } - if (svg_viewBox !== (svg_viewBox = $.get(x))) { + if (svg_viewBox !== (svg_viewBox = x)) { $.attr(svg, "viewBox", svg_viewBox); } - if (custom_element_fooBar !== (custom_element_fooBar = $.get(x))) { + if (custom_element_fooBar !== (custom_element_fooBar = x)) { $.set_custom_element_data(custom_element, "fooBar", custom_element_fooBar); } }); $.close_frag($$anchor, fragment); $.pop(); -} \ No newline at end of file +} diff --git a/packages/svelte/tests/snapshot/samples/export-state/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/export-state/_expected/client/index.svelte.js new file mode 100644 index 000000000000..c9ba1e7c7334 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/export-state/_expected/client/index.svelte.js @@ -0,0 +1,4 @@ +/* index.svelte.js generated by Svelte VERSION */ +import * as $ from "svelte/internal"; + +export const object = $.proxy({ ok: true }); diff --git a/packages/svelte/tests/snapshot/samples/export-state/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/export-state/_expected/server/index.svelte.js new file mode 100644 index 000000000000..770f2e5e4f00 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/export-state/_expected/server/index.svelte.js @@ -0,0 +1,5 @@ +/* index.svelte.js generated by Svelte VERSION */ +import * as $ from "svelte/internal/server"; + +export const object = { ok: true }; + diff --git a/packages/svelte/tests/snapshot/samples/export-state/index.svelte.js b/packages/svelte/tests/snapshot/samples/export-state/index.svelte.js new file mode 100644 index 000000000000..023fcb9b353a --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/export-state/index.svelte.js @@ -0,0 +1,3 @@ +export const object = $state({ + ok: true +}); diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index fb455beeff71..8fb2fbcf6510 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -9,7 +9,7 @@ export default function Function_prop_no_getter($$anchor, $$props) { let count = $.source(0); function onmouseup() { - $.set(count, $.get(count) + 2); + $.set(count, $.proxy($.get(count) + 2)); } /* Init */ @@ -17,7 +17,7 @@ export default function Function_prop_no_getter($$anchor, $$props) { var node = $.child_frag(fragment); Button(node, { - onmousedown: () => $.set(count, $.get(count) + 1), + onmousedown: () => $.set(count, $.proxy($.get(count) + 1)), onmouseup, children: ($$anchor, $$slotProps) => { /* Init */ @@ -31,4 +31,4 @@ export default function Function_prop_no_getter($$anchor, $$props) { $.close_frag($$anchor, fragment); $.pop(); -} +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index 578409e0a497..ffd6a820bc5a 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -25,4 +25,4 @@ export default function Function_prop_no_getter($$payload, $$props) { $$payload.out += `${anchor}`; $.pop(); -} +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js index 263b00e5269d..b4df70d14c37 100644 --- a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js @@ -6,7 +6,7 @@ import * as $ from "svelte/internal"; export default function Svelte_element($$anchor, $$props) { $.push($$props, true); - let tag = $.prop_source($$props, "tag", 'hr', false); + let tag = $.prop_source($$props, "tag", true, 'hr'); /* Init */ var fragment = $.comment($$anchor); var node = $.child_frag(fragment); diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js index 6c4e6ee8b03d..405a45227197 100644 --- a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js @@ -24,4 +24,4 @@ export default function Svelte_element($$payload, $$props) { $$payload.out += `${anchor}`; $.bind_props($$props, { tag }); $.pop(); -} +} \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/_config.js b/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/_config.js deleted file mode 100644 index f47bee71df87..000000000000 --- a/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/_config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { test } from '../../test'; - -export default test({}); diff --git a/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/input.svelte b/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/input.svelte deleted file mode 100644 index eb4e709f4dab..000000000000 --- a/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/input.svelte +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/warnings.json b/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/warnings.json deleted file mode 100644 index 5d2b639c8d43..000000000000 --- a/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/warnings.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "code": "state-not-mutated", - "end": { - "column": 11, - "line": 3 - }, - "message": "count is declared with $state(...) but is never updated. Did you mean to create a function that changes its value?", - "start": { - "column": 6, - "line": 3 - } - } -] 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 e763dbecd27e..8d8454421760 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 @@ -40,6 +40,26 @@ class Todo { > In this example, the compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields +Objects and arrays [are made reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21DviPOwZY3jVpZEtIqUBz9e-UUt9BTj7M784bdmZ21wciq48xsPyGr2MF7Jhl9-kXEKxrCoqNLQS2TOqqgPbWd7cgggU3TgCFCAw-RekJ-3Et4lvByEq-drbe_dlsPichZcFYZrT6amQto2pXw5FO88FUYtG90gUfYi3zvWrYL75vxL57zfA07_zfr23k1vjtt-aZ0bQTcbrDL5ZifZcAxKeS8lzDc8X0xDhJ2ItdbX1jlOZMb9VnjyCoKCfMpfwG975NFVwEAAA==): + +```svelte + + + + + + +

+ {numbers.join(' + ') || 0} + = + {numbers.reduce((a, b) => a + b, 0)} +

+``` + ### What this replaces In non-runes mode, a `let` declaration is treated as reactive state if it is updated at some point. Unlike `$state(...)`, which works anywhere in your app, `let` only behaves this way at the top level of a component. @@ -235,6 +255,8 @@ type MyProps = any; let { a, b, c, ...everythingElse } = $props(); ``` +Props cannot be mutated, unless the parent component uses `bind:`. During development, attempts to mutate props will result in an error. + ### What this replaces `$props` replaces the `export let` and `export { x as y }` syntax for declaring props. It also replaces `$$props` and `$$restProps`, and the little-known `interface $$Props {...}` construct. diff --git a/sites/svelte-5-preview/src/routes/docs/content/02-examples/02-fine-grained-reactivity.md b/sites/svelte-5-preview/src/routes/docs/content/02-examples/02-fine-grained-reactivity.md index efc692f706ea..f257769adf4d 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/02-examples/02-fine-grained-reactivity.md +++ b/sites/svelte-5-preview/src/routes/docs/content/02-examples/02-fine-grained-reactivity.md @@ -44,22 +44,13 @@ In Svelte 4, reactivity centres on the _component_ and the top-level state decla Worse, everything inside the `each` block needs to be checked for updates. When a list gets large enough, this behaviour has the potential to cause performance headaches. -With runes, it's easy to make reactivity _fine-grained_, meaning that things will only update when they need to. Mark `todos` as `$state`, create a `Todo` class with `done` and `text` state fields, then instantiate the class inside `addTodo`: +With runes, it's easy to make reactivity _fine-grained_, meaning that things will only update when they need to: ```diff ``` -In [this version of the app](/#H4sIAAAAAAAAE21SwW6DMAz9lSybBEhTuDNA2mF_sFvpIUtMGzVNUGK6TYh_XxKgSNtOsWO_Z_vZE-2VBk-rw0QNvwKt6Osw0GeK30N0_A00QvC9HZ2IP7UXTg3YdqZDDUjQSutJQ548coT8cCxeQigEhebek_cQJlP0O5TWwJ7Zc-0hJYcQwhfuoY0ikFjj0Y0CrctjTrFxBchZebbi4rMyzfGZF3w_GoHKGuLgypVR5pSndu8skd5qYNqe8syB4FqMmmNIzLbOHODozDImC2IhuERCmpY8RIPFsQqmwZzw_PJfdS5llCGHG5h9AtWT5Ydd4Js8NA3J3kxgzwqy1LyLsEl8YIwl-5kY-CQ7J0PuToDsxvUIxfEO_BsMLFm2NVmX-y5NrcwwIrGmCu1I-2maae17JmXKmB6Bi_O6cO6TkdSupbq1S8WV5UMZWaWCzZQ0igtaefaseGNNR8UZxOXDfnV0wSUf5IqM6m7IulwqTWXsJMlcD-30e7vzvu-6HNpwvVcrVa9A0iocE8zH-QeS_FSn-AIAAA==), editing the `text` of a todo won't cause unrelated things to be updated. - -## Gotchas - -If we only do the first step (adding `$state`) and skip the second (creating a class with state fields), [the app breaks](/#H4sIAAAAAAAAE2VSyU7DMBD9FWOQ0kqVcw9JJA78AbemB2NPWgvXjuxJAVn5d7wkVILbLG_mvVkCHZUGT5tjoIZfgTb0ZZrogeL3lBx_A40QfW9nJ1Kk9cKpCfvBDKgBCVppPenIk0eOsDue9s8xFZPjbAQqa4iDK1dGmfMuY_ckpPSAwhpvNTBtz7vKgeBazJpjBFa5R4Q4wNmZwsGiUgSXm5CuJw_JYNIa2DMN5oyXXLT8YedSvkXgDm5g8JdbjaRE2Ad8k4euI9Wrid2rPSmc6xADbvMdizsgYyzHDlsgbMaASU1DRq49HO5RhC9sSKFD7s6A7Mb1DBtiKcbpl_M_NAqoqm2-tr7fwLTKTDMSa5o4ibSfpgvryAupMyI8AheX9VDcZyNTtlLd-sK4dnlXRjaZsAt5vUn62ueOSr_RDVRcQHy826-Blrrsg1wr0yq2yrYuTKFOSvKF2qkPfx9jub9KW099_LqrlWpUIGmDbobltPwAmGXpQrACAAA=) — toggling the checkboxes won't cause `remaining(todos)` to be recalculated. - -That's because in runes mode, Svelte no longer invalidates everything when you change something inside an `each` block. Previously, Svelte tried to statically determine the dependencies of the mutated value in order to invalidate them, causing confusing bugs related to overfiring (invalidating things that weren't actually affected) and underfiring (missing affected variables). It made apps slower by default and harder to reason about, especially in more complex scenarios. - -In runes mode, the rules around triggering updates are simpler: Only state declared with `$state` or `$derived` or `$props` causes a rerender. In the [broken example](/#H4sIAAAAAAAAE2VSyU7DMBD9FWOQ0kqVcw9JJA78AbemB2NPWgvXjuxJAVn5d7wkVILbLG_mvVkCHZUGT5tjoIZfgTb0ZZrogeL3lBx_A40QfW9nJ1Kk9cKpCfvBDKgBCVppPenIk0eOsDue9s8xFZPjbAQqa4iDK1dGmfMuY_ckpPSAwhpvNTBtz7vKgeBazJpjBFa5R4Q4wNmZwsGiUgSXm5CuJw_JYNIa2DMN5oyXXLT8YedSvkXgDm5g8JdbjaRE2Ad8k4euI9Wrid2rPSmc6xADbvMdizsgYyzHDlsgbMaASU1DRq49HO5RhC9sSKFD7s6A7Mb1DBtiKcbpl_M_NAqoqm2-tr7fwLTKTDMSa5o4ibSfpgvryAupMyI8AheX9VDcZyNTtlLd-sK4dnlXRjaZsAt5vUn62ueOSr_RDVRcQHy826-Blrrsg1wr0yq2yrYuTKFOSvKF2qkPfx9jub9KW099_LqrlWpUIGmDbobltPwAmGXpQrACAAA=), `todo` is declared by the `#each` block, and neither the `text` nor the `done` property are referencing state. One solution would be to turn `text` and `done` into `$state` fields, as shown above. [The other solution](/#H4sIAAAAAAAACmVS226jMBD9lam7EokUmXcKSH3YP9i3EK1ce0isOjayh7QV4t9rG2iq9m0uZ86Z28R6bTCw6jgxK67IKvY8DOzA6GNITrihIYx-cKOXKVIH6fVAbWc7MkhATrkADfwJJAh3x9P-KaZish-tJO0seLwKbbU97zJ2D1NKdySdDc4gN-68KzxKYeRoBEVgkTkixCON3i4aPHZK6DMJNC08JIMrZ3HPDdozXXLR_ENdKPUvAnd4Q0tf2rqHJcJf8QMemgaKvzayF3tYNNchOtrmO3LOs33YODpK4hX0wgQ8bDHCd6pg4Sbhz0j8JsyIS34-fRH_hkSVotiGqMv7om2t7TASOFvFdpV7s820zjVDmRHTIwp5Wa8hAvw_gE6roFrpW7soriwv2qoqCzZTxh_1iae2V647Mj1B0zF5Qfn64t47ttRmH9W36rSIrbouF8WpTB3lc9RDO_38gvn-F3U5tPHFrk7pXqNiFfkR59P8CWtDxuCdAgAA) would be to bind to `todos[i].text` instead of `todo.text` — this way, Svelte picks up the reference to the `todos` `$state` and invalidates it as a whole. Keep in mind that you lose the fine-grained reactivity this way — the whole array is invalidated on every keystroke. +In [this version of the app](/#H4sIAAAAAAAAE2VSy07EMAz8lRCQ2kqovZe2Egf-gBvlEBJ3N9qsUyXuAqr67-TRZSW4xfZ4xh5n5ZM24Hn7tnIUZ-Atf55n_sjpe46Bv4AhCLG3i5Mx03np9EzDiCMZIEZWWc969uBJEJRv79VTKIXitKAkbZE5OAuNGg9lwlZsjeWRpEVvDdTGHsrCgRRGLkZQABaJI0Ac0OIwa9RhUgKXSFg_sLv4qJVFqGoDeKBjatr-qAulXgOwhAsg_WrrieVMfYJvdtf3rHjBwF5ULGvuS4yUtefFH8u9d6Qo2rJJGA-P1xzBF7Usc5JwB6D6IswCub5Vv4T_IcG9orgO3zU3g7HTOC_ELLZhTGU_sV_3fTbWJMR6D0Ie9ysInx7RAuqUvgxZcWf50KjaJNivybs48s5zQ8XD9yOXR5CnD_s18tyXYlB7ZzTg2tk1WWlt4iTJ_m4e1r9X327_oGvmIXyps1V60qB4S26B7X37AXGd34ONAgAA), editing the `text` of a todo won't cause unrelated things to be updated. diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md index 6ffd8a957674..b498862c4737 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md +++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md @@ -39,3 +39,7 @@ These functions run indiscriminately when _anything_ changes. By using `$effect. ``` Note that using `$effect` and `$effect.pre` will put you in [runes mode](/docs/runes) — be sure to update your props and state accordingly. + +## `immutable` + +The `immutable` compiler option is deprecated. Use runes mode instead, where all state is immutable (which means that assigning to `object.property` won't cause updates for anything that is observing `object` itself, or a different property of it).