pull/12110/head
Rich Harris 2 years ago
commit f13056783d

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: support contenteditable binding undefined fallback

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix(types): export CompileResult and Warning

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure element dir properties persist with text changes

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: rename `svelte/reactivity` helpers to include `Svelte` prefix

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: disallow accessing internal Svelte props

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid duplicate signal dependencies

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make media bindings more robust

@ -87,6 +87,7 @@
"cool-roses-trade",
"cuddly-pianos-drop",
"cuddly-points-tickle",
"curly-cooks-cheer",
"curly-lizards-dream",
"curvy-buses-laugh",
"curvy-cups-cough",
@ -108,6 +109,7 @@
"dry-eggs-play",
"dry-eggs-retire",
"dry-fans-march",
"dry-parrots-bathe",
"dry-pillows-exist",
"dull-coins-vanish",
"dull-donkeys-smell",
@ -229,6 +231,7 @@
"happy-suits-film",
"healthy-ants-film",
"healthy-planes-vanish",
"healthy-zebras-accept",
"heavy-comics-move",
"heavy-doors-applaud",
"heavy-ducks-leave",
@ -312,6 +315,7 @@
"lovely-carpets-lick",
"lovely-houses-own",
"lovely-items-turn",
"lovely-ravens-crash",
"lovely-rules-eat",
"lovely-zebras-own",
"lucky-colts-remember",
@ -328,6 +332,7 @@
"mighty-cooks-scream",
"mighty-files-hammer",
"mighty-frogs-obey",
"mighty-shoes-nail",
"modern-apricots-promise",
"modern-fishes-double",
"moody-carrots-lay",
@ -352,6 +357,7 @@
"new-brooms-grin",
"new-rabbits-flow",
"nice-avocados-move",
"nice-jobs-breathe",
"nine-cooks-join",
"ninety-days-visit",
"ninety-dingos-walk",
@ -368,6 +374,7 @@
"old-mails-sneeze",
"old-oranges-compete",
"olive-apples-lick",
"olive-cobras-wonder",
"olive-kangaroos-brake",
"olive-mice-fix",
"olive-moons-act",
@ -387,6 +394,7 @@
"plenty-starfishes-dress",
"plenty-zoos-fix",
"polite-dolphins-care",
"polite-peas-mate",
"polite-pumpkins-guess",
"polite-ravens-study",
"polite-ways-serve",
@ -395,6 +403,7 @@
"poor-seahorses-flash",
"popular-apes-bathe",
"popular-cameras-tie",
"popular-feet-rule",
"popular-games-hug",
"popular-ligers-perform",
"popular-mangos-rest",
@ -449,6 +458,7 @@
"serious-poems-brake",
"serious-socks-cover",
"serious-zebras-scream",
"seven-bees-tell",
"seven-deers-jam",
"seven-garlics-serve",
"seven-hornets-smile",
@ -480,6 +490,7 @@
"silver-sheep-knock",
"six-bears-trade",
"six-boats-shave",
"six-gorillas-obey",
"sixty-items-crash",
"sixty-numbers-hope",
"sixty-pandas-rush",
@ -604,8 +615,10 @@
"tiny-meals-deliver",
"tiny-moose-kiss",
"tiny-taxis-whisper",
"tough-buckets-thank",
"tough-radios-punch",
"tough-tomatoes-explain",
"tricky-laws-bathe",
"twelve-beans-drive",
"twelve-cows-learn",
"twelve-dragons-join",
@ -648,6 +661,7 @@
"wise-ties-clap",
"witty-bikes-shave",
"witty-camels-warn",
"witty-hornets-think",
"witty-readers-provide",
"witty-steaks-dream",
"witty-tomatoes-care",

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow `let props = $props()` and optimize prop read access

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure HMR doesn't mess with anchor nodes

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: improve type arguments for Snippet and $bindable

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: deconflict multiple snippets of the same name

@ -5,7 +5,7 @@
"private": true,
"type": "module",
"license": "MIT",
"packageManager": "pnpm@9.2.0",
"packageManager": "pnpm@9.4.0",
"engines": {
"pnpm": "^9.0.0"
},
@ -30,18 +30,18 @@
},
"devDependencies": {
"@changesets/cli": "^2.27.6",
"@sveltejs/eslint-config": "^7.0.1",
"@sveltejs/eslint-config": "^8.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@vitest/coverage-v8": "^1.2.1",
"eslint": "^9.0.0",
"eslint": "^9.6.0",
"eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0",
"playwright": "^1.41.1",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"typescript": "^5.5.2",
"typescript-eslint": "^8.0.0-alpha.20",
"typescript-eslint": "^8.0.0-alpha.34",
"v8-natives": "^1.2.5",
"vitest": "^1.2.1"
},

@ -1,5 +1,45 @@
# svelte
## 5.0.0-next.169
### Patch Changes
- breaking: rename `svelte/reactivity` helpers to include `Svelte` prefix ([#12248](https://github.com/sveltejs/svelte/pull/12248))
- fix: avoid duplicate signal dependencies ([#12245](https://github.com/sveltejs/svelte/pull/12245))
## 5.0.0-next.168
### Patch Changes
- fix: ensure HMR doesn't mess with anchor nodes ([#12242](https://github.com/sveltejs/svelte/pull/12242))
- fix: deconflict multiple snippets of the same name ([#12221](https://github.com/sveltejs/svelte/pull/12221))
## 5.0.0-next.167
### Patch Changes
- fix: make more types from `svelte/compiler` public ([#12189](https://github.com/sveltejs/svelte/pull/12189))
- fix: support contenteditable binding undefined fallback ([#12210](https://github.com/sveltejs/svelte/pull/12210))
- breaking: prevent usage of arguments keyword in certain places ([#12191](https://github.com/sveltejs/svelte/pull/12191))
- fix(types): export CompileResult and Warning ([#12212](https://github.com/sveltejs/svelte/pull/12212))
- fix: ensure element dir properties persist with text changes ([#12204](https://github.com/sveltejs/svelte/pull/12204))
- fix: disallow accessing internal Svelte props ([#12207](https://github.com/sveltejs/svelte/pull/12207))
- fix: make media bindings more robust ([#12206](https://github.com/sveltejs/svelte/pull/12206))
- fix: allow slot attribute inside snippets ([#12188](https://github.com/sveltejs/svelte/pull/12188))
- feat: allow `let props = $props()` and optimize prop read access ([#12201](https://github.com/sveltejs/svelte/pull/12201))
- feat: improve type arguments for Snippet and $bindable ([#12197](https://github.com/sveltejs/svelte/pull/12197))
## 5.0.0-next.166
### Patch Changes

@ -78,6 +78,10 @@
> Cannot use `$props()` more than once
## props_illegal_name
> Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)
## props_invalid_identifier
> `$props()` can only be used with an object destructuring pattern

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.166",
"version": "5.0.0-next.169",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -306,7 +306,7 @@ declare function $props(): any;
*
* https://svelte-5-preview.vercel.app/docs/runes#$bindable
*/
declare function $bindable<T>(t?: T): T;
declare function $bindable<T>(fallback?: T): T;
/**
* Inspects one or more values whenever they, or the properties they contain, change. Example:

@ -276,6 +276,15 @@ export function props_duplicate(node) {
e(node, "props_duplicate", "Cannot use `$props()` more than once");
}
/**
* Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function props_illegal_name(node) {
e(node, "props_illegal_name", "Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)");
}
/**
* `$props()` can only be used with an object destructuring pattern
* @param {null | number | NodeLike} node

@ -594,7 +594,10 @@ function special(parser) {
type: 'RenderTag',
start,
end: parser.index,
expression: expression
expression: expression,
metadata: {
dynamic: false
}
});
}
}

@ -31,6 +31,7 @@ import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { equal } from '../../utils/assert.js';
/**
* @param {import('#compiler').Script | null} script
@ -969,34 +970,42 @@ const runes_scope_tweaker = {
if (rune === '$props') {
state.analysis.needs_props = true;
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue;
const name =
property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
: /** @type {import('estree').Identifier} */ (property.value).name;
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
binding.prop_alias = alias;
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
if (
initial?.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null
);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
if (node.id.type === 'Identifier') {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name));
binding.initial = null; // else would be $props()
binding.kind = 'rest_prop';
} else {
equal(node.id.type, 'ObjectPattern');
for (const property of node.id.properties) {
if (property.type !== 'Property') continue;
const name =
property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
: /** @type {import('estree').Identifier} */ (property.value).name;
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
binding.prop_alias = alias;
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
if (
initial?.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null
);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
}
}
}
}
@ -1511,6 +1520,13 @@ const common_visitors = {
return;
}
}
},
Component(node, context) {
const binding = context.state.scope.get(
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
node.metadata.dynamic = binding !== null && binding.kind !== 'normal';
}
};

@ -341,6 +341,14 @@ function validate_block_not_empty(node, context) {
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
*/
const validation = {
MemberExpression(node, context) {
if (node.object.type === 'Identifier' && node.property.type === 'Identifier') {
const binding = context.state.scope.get(node.object.name);
if (binding?.kind === 'rest_prop' && node.property.name.startsWith('$$')) {
e.props_illegal_name(node.property);
}
}
},
AssignmentExpression(node, context) {
validate_assignment(node, node.left, context.state);
},
@ -625,6 +633,11 @@ const validation = {
});
},
RenderTag(node, context) {
const callee = unwrap_optional(node.expression).callee;
node.metadata.dynamic =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
context.state.analysis.uses_render_tags = true;
const raw_args = unwrap_optional(node.expression).arguments;
@ -634,7 +647,6 @@ const validation = {
}
}
const callee = unwrap_optional(node.expression).callee;
if (
callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' &&
@ -1240,7 +1252,7 @@ export const validation_runes = merge(validation, a11y_validators, {
e.rune_invalid_arguments(node, rune);
}
if (node.id.type !== 'ObjectPattern') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node);
}
@ -1248,17 +1260,23 @@ export const validation_runes = merge(validation, a11y_validators, {
e.props_invalid_placement(node);
}
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
e.props_invalid_pattern(property);
}
if (node.id.type === 'ObjectPattern') {
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
e.props_invalid_pattern(property);
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (property.key.type === 'Identifier' && property.key.name.startsWith('$$')) {
e.props_illegal_name(property);
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') {
e.props_invalid_pattern(property);
if (value.type !== 'Identifier') {
e.props_invalid_pattern(property);
}
}
}
}

@ -9,6 +9,27 @@ export const global_visitors = {
if (node.name === '$$props') {
return b.id('$$sanitized_props');
}
// Optimize prop access: If it's a member read access, we can use the $$props object directly
const binding = state.scope.get(node.name);
if (
state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write
binding !== null &&
node !== binding.node &&
binding.kind === 'rest_prop'
) {
const parent = path.at(-1);
const grand_parent = path.at(-2);
if (
parent?.type === 'MemberExpression' &&
!parent.computed &&
grand_parent?.type !== 'AssignmentExpression' &&
grand_parent?.type !== 'UpdateExpression'
) {
return b.id('$$props');
}
}
return serialize_get_binding(node, state);
}
},

@ -238,8 +238,6 @@ export const javascript_visitors_runes = {
}
if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */
const seen = ['$$slots', '$$events', '$$legacy'];
@ -247,44 +245,58 @@ export const javascript_visitors_runes = {
seen.push('$$host');
}
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
property.key
);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
initial = b.call('$.proxy', initial);
}
if (declarator.id.type === 'Identifier') {
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (is_prop_source(binding, state)) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {
// RestElement
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(b.literal(declarator.id.name));
}
declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args)));
} else {
assert.equal(declarator.id.type, 'ObjectPattern');
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
property.key
);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
initial = b.call('$.proxy', initial);
}
if (is_prop_source(binding, state)) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {
// RestElement
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
);
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
}

@ -36,6 +36,7 @@ import {
EACH_KEYED,
is_capture_event,
TEMPLATE_FRAGMENT,
TEMPLATE_UNSET_START,
TEMPLATE_USE_IMPORT_NODE,
TRANSITION_GLOBAL,
TRANSITION_IN,
@ -652,9 +653,10 @@ function collect_parent_each_blocks(context) {
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {string} component_name
* @param {import('../types.js').ComponentContext} context
* @param {import('estree').Expression} anchor
* @returns {import('estree').Statement}
*/
function serialize_inline_component(node, component_name, context) {
function serialize_inline_component(node, component_name, context, anchor = context.state.node) {
/** @type {Array<import('estree').Property[] | import('estree').Expression>} */
const props_and_spreads = [];
@ -942,15 +944,16 @@ function serialize_inline_component(node, component_name, context) {
fn = (node_id) => {
return b.call(
'$.component',
node_id,
b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))),
b.arrow(
[b.id(component_name)],
[b.id('$$anchor'), b.id(component_name)],
b.block([
...binding_initializers,
b.stmt(
context.state.options.dev
? b.call('$.validate_dynamic_component', b.thunk(prev(node_id)))
: prev(node_id)
? b.call('$.validate_dynamic_component', b.thunk(prev(b.id('$$anchor'))))
: prev(b.id('$$anchor'))
)
])
)
@ -968,12 +971,12 @@ function serialize_inline_component(node, component_name, context) {
);
statements.push(
b.stmt(b.call('$.css_props', context.state.node, b.thunk(b.object(custom_css_props)))),
b.stmt(fn(b.member(context.state.node, b.id('lastChild'))))
b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))),
b.stmt(fn(b.member(anchor, b.id('lastChild'))))
);
} else {
context.state.template.push('<!>');
statements.push(b.stmt(fn(context.state.node)));
statements.push(b.stmt(fn(anchor)));
}
return statements.length > 1 ? b.block(statements) : statements[0];
@ -1096,13 +1099,12 @@ function serialize_update(statement) {
}
/**
*
* @param {import('../types.js').ComponentClientTransformState} state
* @param {import('estree').Statement[]} update
*/
function serialize_render_stmt(state) {
return state.update.length === 1
? serialize_update(state.update[0])
: b.stmt(b.call('$.template_effect', b.thunk(b.block(state.update))));
function serialize_render_stmt(update) {
return update.length === 1
? serialize_update(update[0])
: b.stmt(b.call('$.template_effect', b.thunk(b.block(update))));
}
/**
@ -1680,14 +1682,35 @@ export const template_visitors = {
process_children(trimmed, expression, false, { ...context, state });
var first = trimmed[0];
/**
* If the first item in an effect is a static slot or render tag, it will clone
* a template but without creating a child effect. In these cases, we need to keep
* the current `effect.nodes.start` undefined, so that it can be populated by
* the item in question
* TODO come up with a better name than `unset`
*/
var unset = false;
if (first.type === 'SlotElement') unset = true;
if (first.type === 'RenderTag' && !first.metadata.dynamic) unset = true;
if (first.type === 'Component' && !first.metadata.dynamic && !context.state.options.hmr) {
unset = true;
}
const use_comment_template = state.template.length === 1 && state.template[0] === '<!>';
if (use_comment_template) {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment')));
body.push(b.var(id, b.call('$.comment', unset && b.literal(unset))));
} else {
let flags = TEMPLATE_FRAGMENT;
if (unset) {
flags |= TEMPLATE_UNSET_START;
}
if (state.metadata.context.template_needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
@ -1709,7 +1732,7 @@ export const template_visitors = {
}
if (state.update.length > 0) {
body.push(serialize_render_stmt(state));
body.push(serialize_render_stmt(state.update));
}
body.push(...state.after_update);
@ -1832,27 +1855,26 @@ export const template_visitors = {
context.state.template.push('<!>');
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
const is_reactive =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
/** @type {import('estree').Expression[]} */
const args = [context.state.node];
for (const arg of raw_args) {
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg))));
}
const args = raw_args.map((arg) =>
b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg)))
);
let snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee));
if (context.state.options.dev) {
snippet_function = b.call('$.validate_snippet', snippet_function);
}
if (is_reactive) {
context.state.init.push(b.stmt(b.call('$.snippet', b.thunk(snippet_function), ...args)));
if (node.metadata.dynamic) {
context.state.init.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
);
} else {
context.state.init.push(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
context.state.node,
...args
)
)
@ -1915,7 +1937,7 @@ export const template_visitors = {
}
if (node.name === 'noscript') {
context.state.template.push('<!>');
context.state.template.push('<noscript></noscript>');
return;
}
if (node.name === 'script') {
@ -1955,6 +1977,7 @@ export const template_visitors = {
let has_content_editable_binding = false;
let img_might_be_lazy = false;
let might_need_event_replaying = false;
let has_direction_attribute = false;
if (is_custom_element) {
// cloneNode is faster, but it does not instantiate the underlying class of the
@ -1970,6 +1993,9 @@ export const template_visitors = {
if (node.name === 'img' && attribute.name === 'loading') {
img_might_be_lazy = true;
}
if (attribute.name === 'dir') {
has_direction_attribute = true;
}
if (
(attribute.name === 'value' || attribute.name === 'checked') &&
!is_text_attribute(attribute)
@ -2153,8 +2179,15 @@ export const template_visitors = {
state.options.preserveComments
);
/** Whether or not we need to wrap the children in `{...}` to avoid declaration conflicts */
const has_declaration = node.fragment.nodes.some((node) => node.type === 'SnippetBlock');
const child_state = has_declaration
? { ...state, init: [], update: [], after_update: [] }
: state;
for (const node of hoisted) {
context.visit(node, state);
context.visit(node, child_state);
}
process_children(
@ -2167,9 +2200,27 @@ export const template_visitors = {
: context.state.node
),
true,
{ ...context, state }
{ ...context, state: child_state }
);
if (has_declaration) {
context.state.init.push(
b.block([
...child_state.init,
child_state.update.length > 0 ? serialize_render_stmt(child_state.update) : b.empty,
...child_state.after_update
])
);
}
if (has_direction_attribute) {
// This fixes an issue with Chromium where updates to text content within an element
// does not update the direction when set to auto. If we just re-assign the dir, this fixes it.
context.state.update.push(
b.stmt(b.assignment('=', b.member(node_id, b.id('dir')), b.member(node_id, b.id('dir'))))
);
}
if (child_locations.length > 0) {
// @ts-expect-error
location.push(child_locations);
@ -2258,7 +2309,7 @@ export const template_visitors = {
/** @type {import('estree').Statement[]} */
const inner = inner_context.state.init;
if (inner_context.state.update.length > 0) {
inner.push(serialize_render_stmt(inner_context.state));
inner.push(serialize_render_stmt(inner_context.state.update));
}
inner.push(...inner_context.state.after_update);
inner.push(
@ -2723,7 +2774,7 @@ export const template_visitors = {
snippet = b.call('$.wrap_snippet', snippet, b.id(context.state.analysis.name));
}
const declaration = b.var(node.expression, snippet);
const declaration = b.const(node.expression, snippet);
// Top-level snippets are hoisted so they can be referenced in the `<script>`
if (context.path.length === 1 && context.path[0].type === 'Fragment') {
@ -2973,27 +3024,26 @@ export const template_visitors = {
}
},
Component(node, context) {
const binding = context.state.scope.get(
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
if (binding !== null && binding.kind !== 'normal') {
if (node.metadata.dynamic) {
// Handle dynamic references to what seems like static inline components
const component = serialize_inline_component(node, '$$component', context);
const component = serialize_inline_component(node, '$$component', context, b.id('$$anchor'));
context.state.init.push(
b.stmt(
b.call(
'$.component',
context.state.node,
// TODO use untrack here to not update when binding changes?
// Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
b.thunk(
/** @type {import('estree').Expression} */ (context.visit(b.member_id(node.name)))
),
b.arrow([b.id('$$component')], b.block([component]))
b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component]))
)
)
);
return;
}
const component = serialize_inline_component(node, node.name, context);
context.state.init.push(component);
},

@ -5,4 +5,4 @@ export type {
PreprocessorGroup,
Processed
} from './preprocess/public';
export type { CompileOptions, ModuleCompileOptions } from './types/index';
export type { CompileOptions, ModuleCompileOptions, CompileResult, Warning } from './types/index';

@ -152,6 +152,9 @@ export interface DebugTag extends BaseNode {
export interface RenderTag extends BaseNode {
type: 'RenderTag';
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
metadata: {
dynamic: boolean;
};
}
type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag;
@ -271,6 +274,9 @@ interface BaseElement extends BaseNode {
export interface Component extends BaseElement {
type: 'Component';
metadata: {
dynamic: boolean;
};
}
interface TitleElement extends BaseElement {

@ -18,6 +18,7 @@ export const TRANSITION_GLOBAL = 1 << 2;
export const TEMPLATE_FRAGMENT = 1;
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
export const TEMPLATE_UNSET_START = 1 << 2;
export const HYDRATION_START = '[';
export const HYDRATION_END = ']';

@ -259,20 +259,24 @@ declare const SnippetReturn: unique symbol;
/**
* The type of a `#snippet` block. You can use it to (for example) express that your component expects a snippet of a certain type:
* ```ts
* let { banner }: { banner: Snippet<{ text: string }> } = $props();
* let { banner }: { banner: Snippet<[{ text: string }]> } = $props();
* ```
* You can only call a snippet through the `{@render ...}` tag.
*
* https://svelte-5-preview.vercel.app/docs/snippets
*
* @template Parameters the parameters that the snippet expects (if any) as a tuple.
*/
export type Snippet<T extends unknown[] = []> =
export type Snippet<Parameters extends unknown[] = []> =
// this conditional allows tuples but not arrays. Arrays would indicate a
// rest parameter type, which is not supported. If rest parameters are added
// in the future, the condition can be removed.
number extends T['length']
number extends Parameters['length']
? never
: {
(
this: void,
...args: T
...args: Parameters
): typeof SnippetReturn & {
_: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"';
};

@ -17,6 +17,7 @@ export const EFFECT_TRANSPARENT = 1 << 15;
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
export const LEGACY_DERIVED_PROP = 1 << 16;
export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const STATE_SYMBOL = Symbol('$state');
export const STATE_FROZEN_SYMBOL = Symbol('$state.frozen');

@ -18,7 +18,7 @@ export function hmr(source) {
/** @type {import("#client").Effect} */
let effect;
block(() => {
block(anchor, 0, () => {
const component = get(source);
if (effect) {

@ -105,7 +105,7 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
}
}
var effect = block(() => {
var effect = block(anchor, 0, () => {
if (input === (input = get_input())) return;
if (is_promise(input)) {

@ -24,12 +24,14 @@ import {
run_out_transitions,
pause_children,
pause_effect,
resume_effect
resume_effect,
get_first_node
} from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen } from '../../utils.js';
import { INERT, STATE_SYMBOL } from '../../constants.js';
import { queue_micro_task } from '../task.js';
import { current_effect } from '../../runtime.js';
/**
* The row of a keyed each block that is currently updating. We track this
@ -54,11 +56,12 @@ export function index(_, i) {
/**
* Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks
* @param {import('#client').EachState} state
* @param {import('#client').EachItem[]} items
* @param {null | Node} controlled_anchor
* @param {Map<any, import("#client").EachItem>} items_map
*/
function pause_effects(items, controlled_anchor, items_map) {
function pause_effects(state, items, controlled_anchor, items_map) {
/** @type {import('#client').TransitionManager[]} */
var transitions = [];
var length = items.length;
@ -77,7 +80,7 @@ function pause_effects(items, controlled_anchor, items_map) {
clear_text_content(parent_node);
parent_node.append(/** @type {Element} */ (controlled_anchor));
items_map.clear();
link(items[0].prev, items[length - 1].next);
link(state, items[0].prev, items[length - 1].next);
}
run_out_transitions(transitions, () => {
@ -85,7 +88,7 @@ function pause_effects(items, controlled_anchor, items_map) {
var item = items[i];
if (!is_controlled) {
items_map.delete(item.k);
link(item.prev, item.next);
link(state, item.prev, item.next);
}
destroy_effect(item.e, !is_controlled);
}
@ -104,7 +107,7 @@ function pause_effects(items, controlled_anchor, items_map) {
*/
export function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
/** @type {import('#client').EachState} */
var state = { flags, items: new Map(), next: null };
var state = { flags, items: new Map(), first: null };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
@ -121,7 +124,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
/** @type {import('#client').Effect | null} */
var fallback = null;
block(() => {
block(anchor, 0, () => {
var collection = get_collection();
var array = is_array(collection)
@ -163,8 +166,8 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
/** @type {Node} */
var child_anchor = hydrate_start;
/** @type {import('#client').EachItem | import('#client').EachState} */
var prev = state;
/** @type {import('#client').EachItem | null} */
var prev = null;
/** @type {import('#client').EachItem} */
var item;
@ -184,7 +187,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
child_anchor = hydrate_anchor(child_anchor);
var value = array[i];
var key = get_key(value, i);
item = create_item(child_anchor, prev, null, value, key, i, render_fn, flags);
item = create_item(child_anchor, state, prev, null, value, key, i, render_fn, flags);
state.items.set(key, item);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
@ -242,14 +245,14 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
var length = array.length;
var items = state.items;
var first = state.next;
var first = state.first;
var current = first;
/** @type {Set<import('#client').EachItem>} */
var seen = new Set();
/** @type {import('#client').EachState | import('#client').EachItem} */
var prev = state;
/** @type {import('#client').EachItem | null} */
var prev = null;
/** @type {Set<import('#client').EachItem>} */
var to_animate = new Set();
@ -293,7 +296,17 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
if (item === undefined) {
var child_anchor = current ? get_first_node(current.e) : anchor;
prev = create_item(child_anchor, prev, prev.next, value, key, i, render_fn, flags);
prev = create_item(
child_anchor,
state,
prev,
prev === null ? state.first : prev.next,
value,
key,
i,
render_fn,
flags
);
items.set(key, prev);
@ -336,9 +349,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
seen.delete(stashed[j]);
}
link(a.prev, b.next);
link(prev, a);
link(b, start);
link(state, a.prev, b.next);
link(state, prev, a);
link(state, b, start);
current = start;
prev = b;
@ -351,9 +364,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
seen.delete(item);
move(item, current, anchor);
link(item.prev, item.next);
link(item, prev.next);
link(prev, item);
link(state, item.prev, item.next);
link(state, item, prev === null ? state.first : prev.next);
link(state, prev, item);
prev = item;
}
@ -403,7 +416,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
}
}
pause_effects(to_destroy, controlled_anchor, items);
pause_effects(state, to_destroy, controlled_anchor, items);
}
if (is_animated) {
@ -413,6 +426,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
}
});
}
/** @type {import('#client').Effect} */ (current_effect).first = state.first && state.first.e;
/** @type {import('#client').Effect} */ (current_effect).last = prev && prev.e;
}
/**
@ -437,7 +453,8 @@ function update_item(item, value, index, type) {
/**
* @template V
* @param {Node} anchor
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachState} state
* @param {import('#client').EachItem | null} prev
* @param {import('#client').EachItem | null} next
* @param {V} value
* @param {unknown} key
@ -446,7 +463,7 @@ function update_item(item, value, index, type) {
* @param {number} flags
* @returns {import('#client').EachItem}
*/
function create_item(anchor, prev, next, value, key, index, render_fn, flags) {
function create_item(anchor, state, prev, next, value, key, index, render_fn, flags) {
var previous_each_item = current_each_item;
try {
@ -468,52 +485,28 @@ function create_item(anchor, prev, next, value, key, index, render_fn, flags) {
next
};
prev.next = item;
if (next !== null) next.prev = item;
current_each_item = item;
item.e = branch(() => render_fn(anchor, v, i));
item.e = branch(() => render_fn(anchor, v, i), hydrating);
return item;
} finally {
current_each_item = previous_each_item;
}
}
item.e.prev = prev && prev.e;
item.e.next = next && next.e;
/**
* @param {import('#client').TemplateNode} dom
* @param {import("#client").Effect} effect
* @returns {import('#client').TemplateNode}
*/
function get_adjusted_first_node(dom, effect) {
if ((dom.nodeType === 3 && /** @type {Text} */ (dom).data === '') || dom.nodeType === 8) {
var adjusted = effect.first;
var next;
while (adjusted !== null) {
next = adjusted.first;
if (adjusted.dom !== null) {
break;
} else if (next === null) {
return /** @type {import('#client').TemplateNode} */ (dom.previousSibling);
}
adjusted = next;
if (prev === null) {
state.first = item;
} else {
prev.next = item;
prev.e.next = item.e;
}
return get_first_node(/** @type {import("#client").Effect} */ (adjusted));
}
return dom;
}
/**
*
* @param {import('#client').Effect} effect
* @returns {import('#client').TemplateNode}
*/
function get_first_node(effect) {
var dom = effect.dom;
if (is_array(dom)) {
return get_adjusted_first_node(dom[0], effect);
if (next !== null) {
next.prev = item;
next.e.prev = item.e;
}
return item;
} finally {
current_each_item = previous_each_item;
}
return get_adjusted_first_node(/** @type {import('#client').TemplateNode} **/ (dom), effect);
}
/**
@ -535,11 +528,20 @@ function move(item, next, anchor) {
}
/**
*
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachState} state
* @param {import('#client').EachItem | null} prev
* @param {import('#client').EachItem | null} next
*/
function link(prev, next) {
prev.next = next;
if (next !== null) next.prev = prev;
function link(state, prev, next) {
if (prev === null) {
state.first = next;
} else {
prev.next = next;
prev.e.next = next && next.e;
}
if (next !== null) {
next.prev = prev;
next.e.prev = prev && prev.e;
}
}

@ -1,30 +1,7 @@
import { derived } from '../../reactivity/deriveds.js';
import { render_effect } from '../../reactivity/effects.js';
import { current_effect, get } from '../../runtime.js';
import { is_array } from '../../utils.js';
import { hydrate_nodes, hydrating } from '../hydration.js';
import { create_fragment_from_html, remove } from '../reconciler.js';
import { push_template_node } from '../template.js';
/**
* @param {import('#client').Effect} effect
* @param {(Element | Comment | Text)[]} to_remove
* @returns {void}
*/
function remove_from_parent_effect(effect, to_remove) {
const dom = effect.dom;
if (is_array(dom)) {
for (let i = dom.length - 1; i >= 0; i--) {
if (to_remove.includes(dom[i])) {
dom.splice(i, 1);
break;
}
}
} else if (dom !== null && to_remove.includes(dom)) {
effect.dom = null;
}
}
import { block, branch, destroy_effect } from '../../reactivity/effects.js';
import { get_start, hydrate_nodes, hydrating } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
/**
* @param {Element | Text | Comment} anchor
@ -34,72 +11,52 @@ function remove_from_parent_effect(effect, to_remove) {
* @returns {void}
*/
export function html(anchor, get_value, svg, mathml) {
const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null;
let value = derived(get_value);
var value = '';
render_effect(() => {
var dom = html_to_dom(anchor, parent_effect, get(value), svg, mathml);
/** @type {import('#client').Effect | null} */
var effect;
if (dom) {
return () => {
if (parent_effect !== null) {
remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]);
}
remove(dom);
};
}
});
}
block(anchor, 0, () => {
if (value === (value = get_value())) return;
/**
* Creates the content for a `@html` tag from its string value,
* inserts it before the target anchor and returns the new nodes.
* @template V
* @param {Element | Text | Comment} target
* @param {import('#client').Effect | null} effect
* @param {V} value
* @param {boolean} svg
* @param {boolean} mathml
* @returns {Element | Comment | (Element | Comment | Text)[]}
*/
function html_to_dom(target, effect, value, svg, mathml) {
if (hydrating) return hydrate_nodes;
var html = value + '';
if (svg) html = `<svg>${html}</svg>`;
else if (mathml) html = `<math>${html}</math>`;
if (effect) {
destroy_effect(effect);
effect = null;
}
// Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
// @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
/** @type {DocumentFragment | Element} */
var node = create_fragment_from_html(html);
if (value === '') return;
if (svg || mathml) {
node = /** @type {Element} */ (node.firstChild);
}
effect = branch(() => {
if (hydrating) {
assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
return;
}
if (node.childNodes.length === 1) {
var child = /** @type {Text | Element | Comment} */ (node.firstChild);
target.before(child);
if (effect !== null) {
push_template_node(child, effect);
}
return child;
}
var html = value + '';
if (svg) html = `<svg>${html}</svg>`;
else if (mathml) html = `<math>${html}</math>`;
var nodes = /** @type {Array<Text | Element | Comment>} */ ([...node.childNodes]);
// Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
// @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
/** @type {DocumentFragment | Element} */
var node = create_fragment_from_html(html);
if (svg || mathml) {
while (node.firstChild) {
target.before(node.firstChild);
}
} else {
target.before(node);
}
if (svg || mathml) {
node = /** @type {Element} */ (node.firstChild);
}
if (effect !== null) {
push_template_node(nodes, effect);
}
assign_nodes(
/** @type {import('#client').TemplateNode} */ (node.firstChild),
/** @type {import('#client').TemplateNode} */ (node.lastChild)
);
return nodes;
if (svg || mathml) {
while (node.firstChild) {
anchor.before(node.firstChild);
}
} else {
anchor.before(node);
}
});
});
}

@ -30,7 +30,7 @@ export function if_block(
var flags = elseif ? EFFECT_TRANSPARENT : 0;
block(() => {
block(anchor, flags, () => {
if (condition === (condition = !!get_condition())) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
@ -78,5 +78,5 @@ export function if_block(
// continue in hydration mode
set_hydrating(true);
}
}, flags);
});
}

@ -16,7 +16,7 @@ export function key_block(anchor, get_key, render_fn) {
/** @type {import('#client').Effect} */
let effect;
block(() => {
block(anchor, 0, () => {
if (safe_not_equal(key, (key = get_key()))) {
if (effect) {
pause_effect(effect);

@ -2,26 +2,25 @@ import { add_snippet_symbol } from '../../../shared/validate.js';
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { branch, block, destroy_effect } from '../../reactivity/effects.js';
import {
current_component_context,
dev_current_component_function,
set_dev_current_component_function
} from '../../runtime.js';
/**
* @template {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} SnippetFn
* @param {import('#client').TemplateNode} anchor
* @param {() => SnippetFn | null | undefined} get_snippet
* @param {import('#client').TemplateNode} node
* @param {(() => any)[]} args
* @returns {void}
*/
export function snippet(get_snippet, node, ...args) {
export function snippet(anchor, get_snippet, ...args) {
/** @type {SnippetFn | null | undefined} */
var snippet;
/** @type {import('#client').Effect | null} */
var snippet_effect;
block(() => {
block(anchor, EFFECT_TRANSPARENT, () => {
if (snippet === (snippet = get_snippet())) return;
if (snippet_effect) {
@ -30,9 +29,9 @@ export function snippet(get_snippet, node, ...args) {
}
if (snippet) {
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(node, ...args));
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args));
}
}, EFFECT_TRANSPARENT);
});
}
/**

@ -1,22 +1,28 @@
import { DEV } from 'esm-env';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
// TODO seems weird that `anchor` is unused here — possible bug?
import { empty } from '../operations.js';
/**
* @template P
* @template {(props: P) => void} C
* @param {import('#client').TemplateNode} anchor
* @param {() => C} get_component
* @param {(component: C) => import('#client').Dom | void} render_fn
* @param {(anchor: import('#client').TemplateNode, component: C) => import('#client').Dom | void} render_fn
* @returns {void}
*/
export function component(get_component, render_fn) {
export function component(anchor, get_component, render_fn) {
/** @type {C} */
let component;
/** @type {import('#client').Effect | null} */
let effect;
block(() => {
var component_anchor = anchor;
// create a dummy anchor for the HMR wrapper, if such there be
if (DEV) component_anchor = empty();
block(anchor, 0, () => {
if (component === (component = get_component())) return;
if (effect) {
@ -25,7 +31,8 @@ export function component(get_component, render_fn) {
}
if (component) {
effect = branch(() => render_fn(component));
if (DEV) anchor.before(component_anchor);
effect = branch(() => render_fn(component_anchor, component));
}
});
}

@ -12,31 +12,9 @@ import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
import { current_component_context, current_effect } from '../../runtime.js';
import { DEV } from 'esm-env';
import { is_array } from '../../utils.js';
import { push_template_node } from '../template.js';
import { assign_nodes } from '../template.js';
import { noop } from '../../../shared/utils.js';
/**
* @param {import('#client').Effect} effect
* @param {Element} from
* @param {Element} to
* @returns {void}
*/
function swap_block_dom(effect, from, to) {
const dom = effect.dom;
if (is_array(dom)) {
for (let i = 0; i < dom.length; i++) {
if (dom[i] === from) {
dom[i] = to;
break;
}
}
} else if (dom === from) {
effect.dom = to;
}
}
/**
* @param {Comment | Element} node
* @param {() => string} get_tag
@ -63,18 +41,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
/** @type {import('#client').Effect | null} */
let effect;
const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
// Remove the the hydrated effect dom entry for our dynamic element
if (hydrating && is_array(parent_effect.dom)) {
var remove_index = parent_effect.dom.indexOf(
/** @type {import('#client').TemplateNode} */ (element)
);
if (remove_index !== -1) {
parent_effect.dom.splice(remove_index, 1);
}
}
/**
* The keyed `{#each ...}` item block, if any, that this element is inside.
* We track this so we can set it when changing the element, allowing any
@ -82,8 +48,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
*/
let each_item_block = current_each_item;
block(() => {
const element_effect = /** @type {import('#client').Effect} */ (current_effect);
block(anchor, 0, () => {
const next_tag = get_tag() || null;
const ns = get_namespace
? get_namespace()
@ -125,6 +90,8 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
assign_nodes(element, element);
if (DEV && location) {
// @ts-expect-error
element.__svelte_meta = {
@ -137,10 +104,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
}
if (prev_element && !hydrating) {
swap_block_dom(element_effect, prev_element, element);
prev_element.remove();
} else {
push_template_node(element, element_effect);
}
if (render_fn) {

@ -2,6 +2,7 @@ import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../
import { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
import { HEAD_EFFECT } from '../../constants.js';
/**
* @type {Node | undefined}
@ -47,7 +48,7 @@ export function head(render_fn) {
}
try {
block(() => render_fn(anchor));
block(null, HEAD_EFFECT, () => render_fn(anchor));
} finally {
if (was_hydrating) {
set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes));

@ -1,6 +1,6 @@
import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of, map_get, map_set } from '../../utils.js';
import { get_descriptors, get_prototype_of } from '../../utils.js';
import {
AttributeAliases,
DelegatedEvents,
@ -166,8 +166,8 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
next.class = '';
}
var setters = map_get(setters_cache, element.nodeName);
if (!setters) map_set(setters_cache, element.nodeName, (setters = get_setters(element)));
var setters = setters_cache.get(element.nodeName);
if (!setters) setters_cache.set(element.nodeName, (setters = get_setters(element)));
// @ts-expect-error
var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {});

@ -22,7 +22,8 @@ function time_ranges_to_array(ranges) {
export function bind_current_time(media, get_value, update) {
/** @type {number} */
var raf_id;
var updating = false;
/** @type {number} */
var value;
// Ideally, listening to timeupdate would be enough, but it fires too infrequently for the currentTime
// binding, which is why we use a raf loop, too. We additionally still listen to timeupdate because
@ -34,22 +35,21 @@ export function bind_current_time(media, get_value, update) {
raf_id = requestAnimationFrame(callback);
}
updating = true;
update(media.currentTime);
var next_value = media.currentTime;
if (value !== next_value) {
update((value = next_value));
}
};
raf_id = requestAnimationFrame(callback);
media.addEventListener('timeupdate', callback);
render_effect(() => {
var value = get_value();
var next_value = Number(get_value());
// through isNaN we also allow number strings, which is more robust
if (!updating && !isNaN(/** @type {any} */ (value))) {
media.currentTime = /** @type {number} */ (value);
if (value !== next_value && !isNaN(/** @type {any} */ (next_value))) {
media.currentTime = value = next_value;
}
updating = false;
});
teardown(() => cancelAnimationFrame(raf_id));
@ -113,22 +113,21 @@ export function bind_ready_state(media, update) {
* @param {(playback_rate: number) => void} update
*/
export function bind_playback_rate(media, get_value, update) {
var updating = false;
// Needs to happen after the element is inserted into the dom, else playback will be set back to 1 by the browser.
// For hydration we could do it immediately but the additional code is not worth the lost microtask.
// Needs to happen after element is inserted into the dom (which is guaranteed by using effect),
// else playback will be set back to 1 by the browser
effect(() => {
var value = get_value();
var value = Number(get_value());
// through isNaN we also allow number strings, which is more robust
if (!isNaN(/** @type {any} */ (value)) && value !== media.playbackRate) {
updating = true;
media.playbackRate = /** @type {number} */ (value);
if (value !== media.playbackRate && !isNaN(value)) {
media.playbackRate = value;
}
});
// Start listening to ratechange events after the element is inserted into the dom,
// else playback will be set to 1 by the browser
effect(() => {
listen(media, ['ratechange'], () => {
if (!updating) update(media.playbackRate);
updating = false;
update(media.playbackRate);
});
});
}
@ -200,9 +199,7 @@ export function bind_paused(media, get_value, update) {
* @param {(volume: number) => void} update
*/
export function bind_volume(media, get_value, update) {
var updating = false;
var callback = () => {
updating = true;
update(media.volume);
};
@ -213,14 +210,11 @@ export function bind_volume(media, get_value, update) {
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = get_value();
var value = Number(get_value());
// through isNaN we also allow number strings, which is more robust
if (!updating && !isNaN(/** @type {any} */ (value))) {
media.volume = /** @type {number} */ (value);
if (value !== media.volume && !isNaN(value)) {
media.volume = value;
}
updating = false;
});
}
@ -230,10 +224,7 @@ export function bind_volume(media, get_value, update) {
* @param {(muted: boolean) => void} update
*/
export function bind_muted(media, get_value, update) {
var updating = false;
var callback = () => {
updating = true;
update(media.muted);
};
@ -244,9 +235,8 @@ export function bind_muted(media, get_value, update) {
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = get_value();
var value = !!get_value();
if (!updating) media.muted = !!value;
updating = false;
if (media.muted !== value) media.muted = value;
});
}

@ -18,7 +18,7 @@ export function bind_content_editable(property, element, get_value, update) {
var value = get_value();
if (element[property] !== value) {
if (value === null) {
if (value == null) {
// @ts-ignore
var non_null_value = element[property];
update(non_null_value);

@ -30,6 +30,17 @@ export function set_hydrate_nodes(nodes) {
hydrate_start = nodes && nodes[0];
}
/**
* When assigning nodes to an effect during hydration, we typically want the hydration boundary comment node
* immediately before `hydrate_start`. In some cases, this comment doesn't exist because we optimized it away.
* TODO it might be worth storing this value separately rather than retrieving it with `previousSibling`
*/
export function get_start() {
return /** @type {import('#client').TemplateNode} */ (
hydrate_start.previousSibling ?? hydrate_start
);
}
/**
* This function is only called when `hydrating` is true. If passed a `<!--[-->` opening
* hydration marker, it finds the corresponding closing marker and sets `hydrate_nodes`

@ -85,13 +85,13 @@ export function first_child(fragment, is_text) {
// text node to hydrate — we must therefore create one
if (is_text && hydrate_start?.nodeType !== 3) {
var text = empty();
var dom = /** @type {import('#client').TemplateNode[]} */ (
/** @type {import('#client').Effect} */ (current_effect).dom
);
var effect = /** @type {import('#client').Effect} */ (current_effect);
dom.unshift(text);
hydrate_start?.before(text);
if (effect.nodes?.start === hydrate_start) {
effect.nodes.start = text;
}
hydrate_start?.before(text);
return text;
}
@ -122,13 +122,7 @@ export function sibling(node, is_text = false) {
// text node to hydrate — we must therefore create one
if (is_text && type !== 3) {
var text = empty();
var dom = /** @type {import('#client').TemplateNode[]} */ (
/** @type {import('#client').Effect} */ (current_effect).dom
);
dom.unshift(text);
next_sibling?.before(text);
return text;
}

@ -1,35 +1,28 @@
import { hydrate_nodes, hydrate_start, hydrating } from './hydration.js';
import { get_start, hydrate_nodes, hydrate_start, hydrating } from './hydration.js';
import { empty } from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { current_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
import { is_array } from '../utils.js';
import {
TEMPLATE_FRAGMENT,
TEMPLATE_UNSET_START,
TEMPLATE_USE_IMPORT_NODE
} from '../../../constants.js';
import { queue_micro_task } from './task.js';
/**
* @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T
* @param {T} dom
* @param {import("#client").Effect} effect
*
* @param {import('#client').TemplateNode | undefined | null} start
* @param {import('#client').TemplateNode} end
* @param {import('#client').TemplateNode | null} anchor
*/
export function push_template_node(
dom,
effect = /** @type {import('#client').Effect} */ (current_effect)
) {
var current_dom = effect.dom;
if (current_dom === null) {
effect.dom = dom;
} else {
if (!is_array(current_dom)) {
current_dom = effect.dom = [current_dom];
}
export function assign_nodes(start, end, anchor = null) {
const effect = /** @type {import('#client').Effect} */ (current_effect);
if (is_array(dom)) {
current_dom.push(...dom);
} else {
current_dom.push(dom);
}
if (effect.nodes === null) {
effect.nodes = { start, anchor, end };
} else if (effect.nodes.start === undefined) {
effect.nodes.start = start;
}
return dom;
}
/**
@ -45,9 +38,13 @@ export function template(content, flags) {
/** @type {Node} */
var node;
var has_start = !content.startsWith('<!>');
var unset = (flags & TEMPLATE_UNSET_START) !== 0;
return () => {
if (hydrating) {
push_template_node(is_fragment ? hydrate_nodes : hydrate_start);
assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
return hydrate_start;
}
@ -56,14 +53,20 @@ export function template(content, flags) {
if (!is_fragment) node = /** @type {Node} */ (node.firstChild);
}
var clone = use_import_node ? document.importNode(node, true) : node.cloneNode(true);
push_template_node(
is_fragment
? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes])
: /** @type {import('#client').TemplateNode} */ (clone)
var clone = /** @type {import('#client').TemplateNode} */ (
use_import_node ? document.importNode(node, true) : node.cloneNode(true)
);
if (is_fragment) {
var first = /** @type {import('#client').TemplateNode} */ (clone.firstChild);
var start = has_start ? first : unset ? undefined : null;
var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild);
assign_nodes(start, end, first);
} else {
assign_nodes(clone, clone);
}
return clone;
};
}
@ -101,37 +104,46 @@ export function template_with_script(content, flags) {
/*#__NO_SIDE_EFFECTS__*/
export function ns_template(content, flags, ns = 'svg') {
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
var fn = template(`<${ns}>${content}</${ns}>`, 0); // we don't need to worry about using importNode for namespaced elements
var wrapped = `<${ns}>${content}</${ns}>`;
/** @type {Element | DocumentFragment} */
var node;
var has_start = !content.startsWith('<!>');
var unset = (flags & TEMPLATE_UNSET_START) !== 0;
return () => {
if (hydrating) {
push_template_node(is_fragment ? hydrate_nodes : hydrate_start);
assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
return hydrate_start;
}
if (!node) {
var wrapper = /** @type {Element} */ (fn());
var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped));
var root = /** @type {Element} */ (fragment.firstChild);
if ((flags & TEMPLATE_FRAGMENT) === 0) {
node = /** @type {Element} */ (wrapper.firstChild);
} else {
if (is_fragment) {
node = document.createDocumentFragment();
while (wrapper.firstChild) {
node.appendChild(wrapper.firstChild);
while (root.firstChild) {
node.appendChild(root.firstChild);
}
} else {
node = /** @type {Element} */ (root.firstChild);
}
}
var clone = node.cloneNode(true);
var clone = /** @type {import('#client').TemplateNode} */ (node.cloneNode(true));
push_template_node(
is_fragment
? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes])
: /** @type {import('#client').TemplateNode} */ (clone)
);
if (is_fragment) {
var first = /** @type {import('#client').TemplateNode} */ (clone.firstChild);
var start = has_start ? first : unset ? undefined : null;
var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild);
assign_nodes(start, end, first);
} else {
assign_nodes(clone, clone);
}
return clone;
};
@ -208,7 +220,11 @@ function run_scripts(node) {
*/
/*#__NO_SIDE_EFFECTS__*/
export function text(anchor) {
if (!hydrating) return push_template_node(empty());
if (!hydrating) {
var t = empty();
assign_nodes(t, t);
return t;
}
var node = hydrate_start;
@ -218,21 +234,26 @@ export function text(anchor) {
anchor.before((node = empty()));
}
push_template_node(node);
assign_nodes(node, node);
return node;
}
export function comment() {
/**
* @param {boolean} unset
*/
export function comment(unset = false) {
// we're not delegating to `template` here for performance reasons
if (hydrating) {
push_template_node(hydrate_nodes);
assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
return hydrate_start;
}
var frag = document.createDocumentFragment();
var anchor = empty();
frag.append(anchor);
push_template_node([anchor]);
assign_nodes(unset ? undefined : null, anchor, anchor);
return frag;
}

@ -31,10 +31,10 @@ import {
DERIVED,
UNOWNED,
CLEAN,
INSPECT_EFFECT
INSPECT_EFFECT,
HEAD_EFFECT
} from '../constants.js';
import { set } from './sources.js';
import { remove } from '../dom/reconciler.js';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
import { define_property } from '../utils.js';
@ -75,16 +75,17 @@ export function push_effect(effect, parent_effect) {
* @param {number} type
* @param {null | (() => void | (() => void))} fn
* @param {boolean} sync
* @param {boolean} push
* @returns {import('#client').Effect}
*/
function create_effect(type, fn, sync) {
function create_effect(type, fn, sync, push = true) {
var is_root = (type & ROOT_EFFECT) !== 0;
/** @type {import('#client').Effect} */
var effect = {
ctx: current_component_context,
deps: null,
dom: null,
nodes: null,
f: type | DIRTY,
first: null,
fn,
@ -120,10 +121,10 @@ function create_effect(type, fn, sync) {
sync &&
effect.deps === null &&
effect.first === null &&
effect.dom === null &&
effect.nodes === null &&
effect.teardown === null;
if (!inert && !is_root) {
if (!inert && !is_root && push) {
if (current_effect !== null) {
push_effect(effect, current_effect);
}
@ -298,16 +299,22 @@ export function template_effect(fn) {
}
/**
* @param {(() => void)} fn
* @param {import('#client').TemplateNode | null} anchor
* @param {number} flags
* @param {(() => void)} fn
*/
export function block(fn, flags = 0) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
export function block(anchor, flags, fn) {
const effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
if (anchor !== null) effect.nodes = { start: null, anchor: null, end: anchor };
return effect;
}
/** @param {(() => void)} fn */
export function branch(fn) {
return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true);
/**
* @param {(() => void)} fn
* @param {boolean} [push]
*/
export function branch(fn, push = true) {
return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true, push);
}
/**
@ -335,13 +342,26 @@ export function execute_effect_teardown(effect) {
* @returns {void}
*/
export function destroy_effect(effect, remove_dom = true) {
var dom = effect.dom;
var removed = false;
if (dom !== null && remove_dom) {
remove(dom);
if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes !== null) {
/** @type {import('#client').TemplateNode | null} */
var node = get_first_node(effect);
var end = effect.nodes.end;
while (node !== null) {
/** @type {import('#client').TemplateNode | null} */
var next =
node === end ? null : /** @type {import('#client').TemplateNode} */ (node.nextSibling);
node.remove();
node = next;
}
removed = true;
}
destroy_effect_children(effect, remove_dom);
destroy_effect_children(effect, remove_dom && !removed);
remove_reactions(effect, 0);
set_signal_status(effect, DESTROYED);
@ -365,13 +385,44 @@ export function destroy_effect(effect, remove_dom = true) {
effect.prev =
effect.teardown =
effect.ctx =
effect.dom =
effect.deps =
effect.parent =
effect.fn =
effect.nodes =
null;
}
/**
* @param {import('#client').Effect} effect
* @returns {import('#client').TemplateNode}
*/
export function get_first_node(effect) {
var nodes = /** @type {NonNullable<typeof effect.nodes>} */ (effect.nodes);
var start = nodes.start;
if (start === undefined) {
// edge case — a snippet or component was the first item inside the effect,
// but it didn't render any DOM. in this case, we return the item's anchor
return /** @type {import('#client').TemplateNode} */ (nodes.anchor);
}
if (start !== null) {
return start;
}
var child = effect.first;
while (child && (child.nodes === null || (child.f & HEAD_EFFECT) !== 0)) {
child = child.next;
}
if (child !== null && child.nodes !== null) {
return get_first_node(child);
}
// in the case that there's no DOM, return the first anchor
return nodes.end;
}
/**
* Detach an effect from the effect tree, freeing up memory and
* reducing the amount of work that happens on subsequent traversals

@ -78,6 +78,7 @@ const rest_props_handler = {
* @param {string} [name]
* @returns {Record<string, unknown>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function rest_props(props, exclude, name) {
return new Proxy(
DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude },

@ -1,4 +1,4 @@
import type { ComponentContext, Dom, Equals, TransitionManager } from '#client';
import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client';
export interface Signal {
/** Flags bitmask */
@ -36,7 +36,11 @@ export interface Derived<V = unknown> extends Value<V>, Reaction {
export interface Effect extends Reaction {
parent: Effect | null;
dom: Dom | null;
nodes: null | {
start: undefined | null | TemplateNode;
anchor: null | TemplateNode;
end: TemplateNode;
};
/** The associated component context */
ctx: null | ComponentContext;
/** The effect function */

@ -555,7 +555,7 @@ function flush_queued_effects(effects) {
// don't know if we need to keep them until they are executed. Doing the check
// here (rather than in `execute_effect`) allows us to skip the work for
// immediate effects.
if (effect.deps === null && effect.first === null && effect.dom === null) {
if (effect.deps === null && effect.first === null && effect.nodes === null) {
if (effect.teardown === null) {
// remove this effect from the graph
unlink_effect(effect);
@ -774,7 +774,10 @@ export function get(signal) {
) {
if (current_dependencies === null) {
current_dependencies = [signal];
} else if (current_dependencies[current_dependencies.length - 1] !== signal) {
} else if (
current_dependencies[current_dependencies.length - 1] !== signal &&
!current_dependencies.includes(signal)
) {
current_dependencies.push(signal);
}
}

@ -71,7 +71,7 @@ export type EachState = {
/** a key -> item lookup */
items: Map<any, EachItem>;
/** head of the linked list of items */
next: EachItem | null;
first: EachItem | null;
};
export type EachItem = {
@ -85,7 +85,7 @@ export type EachItem = {
i: number | Source<number>;
/** key */
k: unknown;
prev: EachItem | EachState;
prev: EachItem | null;
next: EachItem | null;
};

@ -13,33 +13,6 @@ export var object_prototype = Object.prototype;
export var array_prototype = Array.prototype;
export var get_prototype_of = Object.getPrototypeOf;
/** @type {Map<any, any>} */
var map_prototype = Map.prototype;
var map_set_method = map_prototype.set;
var map_get_method = map_prototype.get;
/**
* @template K
* @template V
* @param {Map<K, V>} map
* @param {K} key
* @param {V} value
*/
export function map_set(map, key, value) {
map_set_method.call(map, key, value);
}
/**
* @template K
* @template V
* @param {Map<K, V>} map
* @param {K} key
* @return {V}
*/
export function map_get(map, key) {
return map_get_method.call(map, key);
}
/**
* @param {any} thing
* @returns {thing is Function}

@ -5,7 +5,7 @@ import { get } from '../internal/client/runtime.js';
var inited = false;
export class ReactiveDate extends Date {
export class SvelteDate extends Date {
#time = source(super.getTime());
/** @type {Map<keyof Date, Source<unknown>>} */
@ -21,7 +21,7 @@ export class ReactiveDate extends Date {
#init() {
inited = true;
var proto = ReactiveDate.prototype;
var proto = SvelteDate.prototype;
var date_proto = Date.prototype;
var methods = /** @type {Array<keyof Date & string>} */ (

@ -1,4 +1,29 @@
export { ReactiveDate as Date } from './date.js';
export { ReactiveSet as Set } from './set.js';
export { ReactiveMap as Map } from './map.js';
export { ReactiveURL as URL, ReactiveURLSearchParams as URLSearchParams } from './url.js';
export { SvelteDate } from './date.js';
export { SvelteSet } from './set.js';
export { SvelteMap } from './map.js';
export { SvelteURL, SvelteURLSearchParams } from './url.js';
/** @deprecated Use `SvelteDate` instead */
export function Date() {
throw new Error('Date has been removed, use SvelteDate instead.');
}
/** @deprecated Use `SvelteSet` instead */
export function Set() {
throw new Error('Set has been removed, use SvelteSet instead.');
}
/** @deprecated Use `SvelteMap` instead */
export function Map() {
throw new Error('Map has been removed, use SvelteMap instead.');
}
/** @deprecated Use `SvelteURL` instead */
export function URL() {
throw new Error('URL has been removed, use SvelteURL instead.');
}
/** @deprecated Use `SvelteURLSearchParams` instead */
export function URLSearchParams() {
throw new Error('URLSearchParams has been removed, use SvelteURLSearchParams instead.');
}

@ -1,5 +1,30 @@
export const Date = globalThis.Date;
export const Set = globalThis.Set;
export const Map = globalThis.Map;
export const URL = globalThis.URL;
export const URLSearchParams = globalThis.URLSearchParams;
export const SvelteDate = globalThis.Date;
export const SvelteSet = globalThis.Set;
export const SvelteMap = globalThis.Map;
export const SvelteURL = globalThis.URL;
export const SvelteURLSearchParams = globalThis.URLSearchParams;
/** @deprecated Use `SvelteDate` instead */
export function Date() {
throw new Error('Date has been removed, use SvelteDate instead.');
}
/** @deprecated Use `SvelteSet` instead */
export function Set() {
throw new Error('Set has been removed, use SvelteSet instead.');
}
/** @deprecated Use `SvelteMap` instead */
export function Map() {
throw new Error('Map has been removed, use SvelteMap instead.');
}
/** @deprecated Use `SvelteURL` instead */
export function URL() {
throw new Error('URL has been removed, use SvelteURL instead.');
}
/** @deprecated Use `SvelteURLSearchParams` instead */
export function URLSearchParams() {
throw new Error('URLSearchParams has been removed, use SvelteURLSearchParams instead.');
}

@ -8,7 +8,7 @@ import { increment } from './utils.js';
* @template V
* @extends {Map<K, V>}
*/
export class ReactiveMap extends Map {
export class SvelteMap extends Map {
/** @type {Map<K, import('#client').Source<number>>} */
#sources = new Map();
#version = source(0);

@ -1,10 +1,10 @@
import { render_effect, effect_root } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../index-client.js';
import { ReactiveMap } from './map.js';
import { SvelteMap } from './map.js';
import { assert, test } from 'vitest';
test('map.values()', () => {
const map = new ReactiveMap([
const map = new SvelteMap([
[1, 1],
[2, 2],
[3, 3],
@ -65,7 +65,7 @@ test('map.values()', () => {
});
test('map.get(...)', () => {
const map = new ReactiveMap([
const map = new SvelteMap([
[1, 1],
[2, 2],
[3, 3]
@ -101,7 +101,7 @@ test('map.get(...)', () => {
});
test('map.has(...)', () => {
const map = new ReactiveMap([
const map = new SvelteMap([
[1, 1],
[2, 2],
[3, 3]
@ -148,7 +148,7 @@ test('map.has(...)', () => {
});
test('map.forEach(...)', () => {
const map = new ReactiveMap([
const map = new SvelteMap([
[1, 1],
[2, 2],
[3, 3]
@ -169,7 +169,7 @@ test('map.forEach(...)', () => {
});
test('map.delete(...)', () => {
const map = new ReactiveMap([
const map = new SvelteMap([
[1, 1],
[2, 2],
[3, 3]
@ -182,7 +182,7 @@ test('map.delete(...)', () => {
});
test('map handling of undefined values', () => {
const map = new ReactiveMap();
const map = new SvelteMap();
const log: any = [];
@ -208,7 +208,7 @@ test('map handling of undefined values', () => {
});
test('not invoking reactivity when value is not in the map after changes', () => {
const map = new ReactiveMap([[1, 1]]);
const map = new SvelteMap([[1, 1]]);
const log: any = [];
@ -236,5 +236,5 @@ test('not invoking reactivity when value is not in the map after changes', () =>
});
test('Map.instanceOf', () => {
assert.equal(new ReactiveMap() instanceof Map, true);
assert.equal(new SvelteMap() instanceof Map, true);
});

@ -12,7 +12,7 @@ var inited = false;
* @template T
* @extends {Set<T>}
*/
export class ReactiveSet extends Set {
export class SvelteSet extends Set {
/** @type {Map<T, import('#client').Source<boolean>>} */
#sources = new Map();
#version = source(0);
@ -41,7 +41,7 @@ export class ReactiveSet extends Set {
#init() {
inited = true;
var proto = ReactiveSet.prototype;
var proto = SvelteSet.prototype;
var set_proto = Set.prototype;
for (const method of read_methods) {
@ -59,7 +59,7 @@ export class ReactiveSet extends Set {
get(this.#version);
// @ts-ignore
var set = /** @type {Set<T>} */ (set_proto[method].apply(this, v));
return new ReactiveSet(set);
return new SvelteSet(set);
};
}
}

@ -1,10 +1,10 @@
import { render_effect, effect_root } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../index-client.js';
import { ReactiveSet } from './set.js';
import { SvelteSet } from './set.js';
import { assert, test } from 'vitest';
test('set.values()', () => {
const set = new ReactiveSet([1, 2, 3, 4, 5]);
const set = new SvelteSet([1, 2, 3, 4, 5]);
const log: any = [];
@ -36,7 +36,7 @@ test('set.values()', () => {
});
test('set.has(...)', () => {
const set = new ReactiveSet([1, 2, 3]);
const set = new SvelteSet([1, 2, 3]);
const log: any = [];
@ -79,7 +79,7 @@ test('set.has(...)', () => {
});
test('set.delete(...)', () => {
const set = new ReactiveSet([1, 2, 3]);
const set = new SvelteSet([1, 2, 3]);
assert.equal(set.delete(3), true);
assert.equal(set.delete(3), false);
@ -88,7 +88,7 @@ test('set.delete(...)', () => {
});
test('set.forEach()', () => {
const set = new ReactiveSet([1, 2, 3, 4, 5]);
const set = new SvelteSet([1, 2, 3, 4, 5]);
const log: any = [];
@ -108,7 +108,7 @@ test('set.forEach()', () => {
});
test('not invoking reactivity when value is not in the set after changes', () => {
const set = new ReactiveSet([1, 2]);
const set = new SvelteSet([1, 2]);
const log: any = [];
@ -155,5 +155,5 @@ test('not invoking reactivity when value is not in the set after changes', () =>
});
test('Set.instanceOf', () => {
assert.equal(new ReactiveSet() instanceof Set, true);
assert.equal(new SvelteSet() instanceof Set, true);
});

@ -4,7 +4,7 @@ import { increment } from './utils.js';
const REPLACE = Symbol();
export class ReactiveURL extends URL {
export class SvelteURL extends URL {
#protocol = source(super.protocol);
#username = source(super.username);
#password = source(super.password);
@ -12,7 +12,7 @@ export class ReactiveURL extends URL {
#port = source(super.port);
#pathname = source(super.pathname);
#hash = source(super.hash);
#searchParams = new ReactiveURLSearchParams();
#searchParams = new SvelteURLSearchParams();
/**
* @param {string | URL} url
@ -153,7 +153,7 @@ export class ReactiveURL extends URL {
}
}
export class ReactiveURLSearchParams extends URLSearchParams {
export class SvelteURLSearchParams extends URLSearchParams {
#version = source(0);
/**

@ -1,10 +1,10 @@
import { render_effect, effect_root } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../index-client.js';
import { ReactiveURL, ReactiveURLSearchParams } from './url.js';
import { SvelteURL, SvelteURLSearchParams } from './url.js';
import { assert, test } from 'vitest';
test('url.hash', () => {
const url = new ReactiveURL('http://google.com');
const url = new SvelteURL('http://google.com');
const log: any = [];
const cleanup = effect_root(() => {
@ -32,7 +32,7 @@ test('url.hash', () => {
});
test('url.searchParams', () => {
const url = new ReactiveURL('https://svelte.dev?foo=bar&t=123');
const url = new SvelteURL('https://svelte.dev?foo=bar&t=123');
const log: any = [];
const cleanup = effect_root(() => {
@ -78,7 +78,7 @@ test('url.searchParams', () => {
});
test('URLSearchParams', () => {
const params = new ReactiveURLSearchParams();
const params = new SvelteURLSearchParams();
const log: any = [];
const cleanup = effect_root(() => {

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.166';
export const VERSION = '5.0.0-next.169';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'props_illegal_name',
message:
'Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)'
}
});

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'props_illegal_name',
message:
'Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)'
}
});

@ -0,0 +1,4 @@
<script>
let props = $props();
props.$$slots;
</script>

@ -3,4 +3,4 @@
<svelte:element this={"div"} />
<!-- we don't try to fix this bug, we just leave it as-is -->
<svelte:element this="h{n}" />
<svelte:element this="h{n}" />

@ -27,8 +27,8 @@
"parameters": [
{
"type": "Identifier",
"name": "msg",
"start": 43,
"end": 46,
"loc": {
"start": {
"line": 3,
@ -39,7 +39,7 @@
"column": 25
}
},
"end": 46,
"name": "msg",
"typeAnnotation": {
"type": "TSTypeAnnotation",
"start": 46,

@ -44,6 +44,7 @@ export function equal(a, b, message) {
/**
* @param {any} condition
* @param {string} [message]
* @returns {asserts condition}
*/
export function ok(condition, message) {
if (!condition) throw new Error(message || `Expected ${condition} to be truthy`);

@ -0,0 +1,29 @@
import { test, ok } from '../../assert';
export default test({
mode: ['client'],
async test({ assert, target }) {
const audio = target.querySelector('audio');
const button = target.querySelector('button');
ok(audio);
assert.equal(audio.muted, false);
audio.muted = true;
audio.dispatchEvent(new CustomEvent('volumechange'));
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.muted, true, 'event');
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.muted, false, 'click 1');
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.muted, true, 'click 2');
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.muted, false, 'click 3');
}
});

@ -0,0 +1,6 @@
<script>
let muted = $state(false);
</script>
<audio bind:muted></audio>
<button onclick={() => (muted = !muted)}>toggle</button>

@ -0,0 +1,29 @@
import { test, ok } from '../../assert';
export default test({
mode: ['client'],
async test({ assert, target }) {
const audio = target.querySelector('audio');
const button = target.querySelector('button');
ok(audio);
assert.equal(audio.playbackRate, 0.5);
audio.playbackRate = 1.0;
audio.dispatchEvent(new CustomEvent('ratechange'));
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.playbackRate, 1.0);
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.playbackRate, 2);
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.playbackRate, 3);
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.playbackRate, 4);
}
});

@ -0,0 +1,6 @@
<script>
let playbackRate = $state(0.5);
</script>
<audio bind:playbackRate></audio>
<button onclick={() => (playbackRate += 1)}>increment</button>

@ -0,0 +1,29 @@
import { test, ok } from '../../assert';
export default test({
mode: ['client'],
async test({ assert, target }) {
const audio = target.querySelector('audio');
const button = target.querySelector('button');
ok(audio);
assert.equal(audio.volume, 0.1);
audio.volume = 0.2;
audio.dispatchEvent(new CustomEvent('volumechange'));
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.volume, 0.2);
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.volume, 0.2 + 0.1); // JavaScript can't add floating point numbers correctly
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.volume, 0.2 + 0.1 + 0.1);
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.volume, 0.2 + 0.1 + 0.1 + 0.1);
}
});

@ -0,0 +1,6 @@
<script>
let volume = $state(0.1);
</script>
<audio bind:volume></audio>
<button onclick={() => (volume += 0.1)}>increment</button>

@ -1,27 +0,0 @@
import { test } from '../../test';
export default test({
test({ assert, target, variant }) {
// if created on client side, should not build noscript
if (variant === 'dom') {
assert.equal(target.querySelectorAll('noscript').length, 0);
assert.htmlEqual(
target.innerHTML,
`
<div>foo</div>
<div>foo<div>foo</div></div>
`
);
} else {
assert.equal(target.querySelectorAll('noscript').length, 3);
assert.htmlEqual(
target.innerHTML,
`
<noscript>foo</noscript>
<div>foo<noscript>foo</noscript></div>
<div>foo<div>foo<noscript>foo</noscript></div></div>
`
);
}
}
});

@ -1,5 +0,0 @@
<noscript>foo</noscript>
<div>foo<noscript>foo</noscript></div>
<div>foo<div>foo<noscript>foo</noscript></div></div>

@ -0,0 +1,9 @@
<script>
let {children} = $props()
let innerText = $state()
$inspect(innerText);
</script>
<div contenteditable="true" bind:innerHTML={innerText}>
{@render children()}
</div>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<div contenteditable="true">Test</div>`
});

@ -0,0 +1,7 @@
<script>
import Test from './Test.svelte'
</script>
<Test>
Test
</Test>

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
let [btn1, btn2] = target.querySelectorAll('button');
flushSync(() => {
btn1?.click();
});
assert.htmlEqual(target.innerHTML, `<button>hide</button><button>show</button`);
flushSync(() => {
btn2?.click();
});
assert.htmlEqual(
target.innerHTML,
`<h1>John Doe</h1><p>Body</p><button>hide</button><button>show</button>`
);
}
});

@ -0,0 +1,27 @@
<script>
let data = $state({
event: {
author: 'John Doe',
body: 'Body'
}
});
const { event } = $derived(data);
</script>
{#if event}
<h1>{event.author}</h1>
<p>{event.body}</p>
{/if}
<button onclick={() => {
data = {}
}}>hide</button>
<button onclick={() => {
data = {
event: {
author: 'John Doe',
body: 'Body'
}
}
}}>show</button>

@ -0,0 +1,27 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<ul><li>test (1) <span style="background-color: red; width: 20px; height: 20px; display: inline-block;"></span></li><li>test 2 (2)</li><li>test 3 (3)</li></ul><button>Swap items 1 &amp; 3</button>`,
async test({ assert, target }) {
const [btn1] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
flushSync(() => {
btn1.click();
});
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<ul><li>test (1) <span style="background-color: red; width: 20px; height: 20px; display: inline-block;"></span></li><li>test 2 (2)</li><li>test 3 (3)</li></ul><button>Swap items 1 &amp; 3</button>`
);
}
});

@ -0,0 +1,26 @@
<script>
const items = $state([
{ name: 'test', id: 1, color: 'red' },
{ name: 'test 2', id: 2 },
{ name: 'test 3', id: 3 },
]);
const onclick = () => {
const from = 0;
const to = 2;
items.splice(to, 0, items.splice(from, 1)[0]);
};
</script>
{#snippet renderItem(item)}
<li>
{item.name} ({item.id})
{#if item.color}<span style="background-color: {item.color}; width: 20px; height: 20px; display: inline-block;"></span>{/if}
</li>
{/snippet}
<ul>
{#each items as item (item.id)}
{@render renderItem(item)}
{/each}
</ul>
<button {onclick}>Swap items 1 & 3</button>

@ -0,0 +1,27 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<ul><li>test (1)</li> <span style="background-color: red; width: 20px; height: 20px; display: inline-block;"></span><li>test 2 (2)</li><li>test 3 (3)</li></ul><button>Swap items 1 &amp; 3</button>`,
async test({ assert, target }) {
const [btn1] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
flushSync(() => {
btn1.click();
});
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<ul><li>test (1)</li><span style="background-color: red; width: 20px; height: 20px; display: inline-block;"></span><li>test 2 (2)</li><li>test 3 (3)</li></ul><button>Swap items 1 &amp; 3</button>`
);
}
});

@ -0,0 +1,26 @@
<script>
const items = $state([
{ name: 'test', id: 1, color: 'red' },
{ name: 'test 2', id: 2 },
{ name: 'test 3', id: 3 },
]);
const onclick = () => {
const from = 0;
const to = 2;
items.splice(to, 0, items.splice(from, 1)[0]);
};
</script>
{#snippet renderItem(item)}
<li>
{item.name} ({item.id})
</li>
{#if item.color}<span style="background-color: {item.color}; width: 20px; height: 20px; display: inline-block;"></span>{/if}
{/snippet}
<ul>
{#each items as item (item.id)}
{@render renderItem(item)}
{/each}
</ul>
<button {onclick}>Swap items 1 & 3</button>

@ -0,0 +1,42 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>Add new message</button><p>first</p><p>message 1</p>`,
async test({ assert, target }) {
/**
* @type {{ click: () => void; }}
*/
let btn1;
[btn1] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>Add new message</button><p>first</p><p>message 1</p><p>message 2</p>`
);
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`<button>Add new message</button><p>first</p><p>message 1</p><p>message 2</p>`
);
flushSync(() => {
btn1.click();
});
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`<button>Add new message</button><p>first</p><p>message 1</p><p>message 2</p><p>message 3</p>`
);
}
});

@ -0,0 +1,23 @@
<script>
let messages = $state([{id: 1, content: "message 1"}]);
function add() {
const newId = messages.length + 1
messages.push({id: 0, tmpId: newId, content: `message ${newId}`})
queueMicrotask(() => {
const msg = messages.find((m) => m.tmpId === newId && m.id === 0)
msg.tmpId = ""
msg.id = newId
})
}
</script>
<button onclick={add}>Add new message</button>
{#each messages as msg, i (`${msg.id}_${msg.tmpId ?? ""}`)}
{#if i === 0}
<p>first</p>
{/if}
<p>{msg.content}</p>
{/each}

@ -0,0 +1,21 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
hmr: true
},
html: `<button>toggle</button>`,
test({ assert, target }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<button>toggle</button><p>hello</p>`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<button>toggle</button>`);
}
});

@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
let open = $state(false);
</script>
<button onclick={() => (open = !open)}>toggle</button>
{#if open}
<Child />
{/if}

@ -1,7 +1,7 @@
<script>
import { Date } from 'svelte/reactivity';
import { SvelteDate } from 'svelte/reactivity';
let date = new Date('2024-02-23T15:00:00Z');
let date = new SvelteDate('2024-02-23T15:00:00Z');
</script>
<div>getSeconds: {date.getUTCSeconds()}</div>

@ -1,7 +1,7 @@
<script>
import { Map } from 'svelte/reactivity';
import { SvelteMap } from 'svelte/reactivity';
let state = new Map([[0, 0]]);
let state = new SvelteMap([[0, 0]]);
</script>
<button

@ -1,7 +1,7 @@
<script>
import { Set } from 'svelte/reactivity';
import { SvelteSet } from 'svelte/reactivity';
let state = new Set([0]);
let state = new SvelteSet([0]);
</script>
<button onclick={() => state.delete(0)}>delete initial</button>

@ -1,13 +1,13 @@
<script>
import { Set as ReactiveSet, Map as ReactiveMap } from 'svelte/reactivity';
import { SvelteSet, SvelteMap } from 'svelte/reactivity';
let map = new Map();
let set = new Set();
let rmap = new ReactiveMap();
let rset = new ReactiveSet();
let rmap = new SvelteMap();
let rset = new SvelteSet();
</script>
<div>{rset.entries()} {rset.keys()} {rset.values()}</div>
<div>{set.entries()} {set.keys()} {set.values()}</div>
<div>{rmap.entries()} {rmap.keys()} {rmap.values()}</div>
<div>{map.entries()} {map.keys()} {map.values()}</div>
<div>{map.entries()} {map.keys()} {map.values()}</div>

@ -1,7 +1,7 @@
<script>
import { URL } from 'svelte/reactivity';
import { SvelteURL } from 'svelte/reactivity';
let url = new URL('https://svelte.dev/repl/hello-world?version=5.0');
let url = new SvelteURL('https://svelte.dev/repl/hello-world?version=5.0');
</script>
<div>href: {url.href}</div>

@ -0,0 +1,24 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
btn?.click();
btn?.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<button>push</button><div style="display: grid; grid-template-columns: 1fr 1fr"><div><p style="color: red">1</p><p style="color: red">2</p><p style="color: red">3</p>
<p style="color: red">4</p><p style="color: red">5</p><p style="color: red">6</p></div><div><p style="color: blue">1</p><p style="color: blue">2</p><p style="color: blue">3</p>
<p style="color: blue">4</p><p style="color: blue">5</p><p style="color: blue">6</p></div></div>
`
);
}
});

@ -0,0 +1,29 @@
<script>
let numbers = $state([1, 2, 3]);
</script>
<button onclick={() => numbers.push(numbers.length + 1)}>
push
</button>
<div style="display: grid; grid-template-columns: 1fr 1fr">
<div>
{#snippet x(n)}
<p style="color: red">{n}</p>
{/snippet}
{#each numbers as n}
{@render x(n)}
{/each}
</div>
<div>
{#snippet x(n)}
<p style="color: blue">{n}</p>
{/snippet}
{#each numbers as n}
{@render x(n)}
{/each}
</div>
</div>

@ -1,6 +1,6 @@
<script>
import { Set } from 'svelte/reactivity';
const set = new Set();
import { SvelteSet } from 'svelte/reactivity';
const set = new SvelteSet();
</script>
<form onsubmit={e => {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save