Merge remote-tracking branch 'origin/main' into source-to-state-in-classes

source-to-state-in-class-belt-and-braces
paoloricciuti 2 months ago
commit 2ea891e37b

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: account for mounting when `select_option` in `attribute_effect`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: do not proxify the value assigned to a derived

@ -7,6 +7,7 @@ packages/**/config/*.js
# packages/svelte
packages/svelte/messages/**/*.md
packages/svelte/scripts/_bundle.js
packages/svelte/src/compiler/errors.js
packages/svelte/src/compiler/warnings.js
packages/svelte/src/internal/client/errors.js
@ -25,8 +26,7 @@ packages/svelte/tests/hydration/samples/*/_expected.html
packages/svelte/tests/hydration/samples/*/_override.html
packages/svelte/types
packages/svelte/compiler/index.js
playgrounds/sandbox/input/**.svelte
playgrounds/sandbox/output
playgrounds/sandbox/src/*
# sites/svelte.dev
sites/svelte.dev/static/svelte-app.json

@ -83,7 +83,7 @@ If you're using tools like Rollup or Webpack instead, install their respective S
When using TypeScript, make sure your `tsconfig.json` is setup correctly.
- Use a [`target`](https://www.typescriptlang.org/tsconfig/#target) of at least `ES2022`, or a `target` of at least `ES2015` alongside [`useDefineForClassFields`](https://www.typescriptlang.org/tsconfig/#useDefineForClassFields). This ensures that rune declarations on class fields are not messed with, which would break the Svelte compiler
- Use a [`target`](https://www.typescriptlang.org/tsconfig/#target) of at least `ES2015` so classes are not compiled to functions
- Set [`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax) to `true` so that imports are left as-is
- Set [`isolatedModules`](https://www.typescriptlang.org/tsconfig/#isolatedModules) to `true` so that each file is looked at in isolation. TypeScript has a few features which require cross-file analysis and compilation, which the Svelte compiler and tooling like Vite don't do.

@ -74,6 +74,12 @@ Effect cannot be created inside a `$derived` value that was not itself created i
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
```
### get_abort_signal_outside_reaction
```
`getAbortSignal()` can only be called inside an effect or derived
```
### hydration_failed
```

@ -87,6 +87,7 @@ export default [
'**/*.d.ts',
'**/tests',
'packages/svelte/scripts/process-messages/templates/*.js',
'packages/svelte/scripts/_bundle.js',
'packages/svelte/src/compiler/errors.js',
'packages/svelte/src/internal/client/errors.js',
'packages/svelte/src/internal/client/warnings.js',

@ -1,5 +1,33 @@
# svelte
## 5.35.2
### Patch Changes
- fix: bump esrap ([#16295](https://github.com/sveltejs/svelte/pull/16295))
## 5.35.1
### Patch Changes
- feat: add parent hierarchy to `__svelte_meta` objects ([#16255](https://github.com/sveltejs/svelte/pull/16255))
## 5.35.0
### Minor Changes
- feat: add `getAbortSignal()` ([#16266](https://github.com/sveltejs/svelte/pull/16266))
### Patch Changes
- chore: simplify props ([#16270](https://github.com/sveltejs/svelte/pull/16270))
## 5.34.9
### Patch Changes
- fix: ensure unowned deriveds can add themselves as reactions while connected ([#16249](https://github.com/sveltejs/svelte/pull/16249))
## 5.34.8
### Patch Changes

@ -48,6 +48,10 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
## get_abort_signal_outside_reaction
> `getAbortSignal()` can only be called inside an effect or derived
## hydration_failed
> Failed to hydrate the application

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.34.8",
"version": "5.35.2",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -164,14 +164,14 @@
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"@sveltejs/acorn-typescript": "^1.0.5",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
"esrap": "^1.4.8",
"esrap": "^2.1.0",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -118,36 +118,40 @@ const bundle = await bundle_code(
).js.code
);
if (!bundle.includes('hydrate_node') && !bundle.includes('hydrate_next')) {
// eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`);
} else {
failed = true;
// eslint-disable-next-line no-console
console.error(`❌ Hydration code not treeshakeable`);
}
/**
* @param {string} case_name
* @param {string[]} strings
*/
function check_bundle(case_name, ...strings) {
for (const string of strings) {
const index = bundle.indexOf(string);
if (index >= 0) {
// eslint-disable-next-line no-console
console.error(`${case_name} not treeshakeable`);
failed = true;
if (!bundle.includes('component_context.l')) {
// eslint-disable-next-line no-console
console.error(`✅ Legacy code treeshakeable`);
} else {
failed = true;
let lines = bundle.slice(index - 500, index + 500).split('\n');
const target_line = lines.findIndex((line) => line.includes(string));
// mark the failed line
lines = lines
.map((line, i) => (i === target_line ? `> ${line}` : `| ${line}`))
.slice(target_line - 5, target_line + 6);
// eslint-disable-next-line no-console
console.error('The first failed line:\n' + lines.join('\n'));
return;
}
}
// eslint-disable-next-line no-console
console.error(`❌ Legacy code not treeshakeable`);
console.error(`${case_name} treeshakeable`);
}
if (!bundle.includes(`'CreatedAt'`)) {
// eslint-disable-next-line no-console
console.error(`$inspect.trace code treeshakeable`);
} else {
failed = true;
// eslint-disable-next-line no-console
console.error(`$inspect.trace code not treeshakeable`);
}
check_bundle('Hydration code', 'hydrate_node', 'hydrate_next');
check_bundle('Legacy code', 'component_context.l');
check_bundle('$inspect.trace', `'CreatedAt'`);
if (failed) {
// eslint-disable-next-line no-console
console.error(bundle);
console.error('Full bundle at', path.resolve('scripts/_bundle.js'));
fs.writeFileSync('scripts/_bundle.js', bundle);
}

@ -1,9 +1,14 @@
/** @import { Node } from 'esrap/languages/ts' */
/** @import * as ESTree from 'estree' */
/** @import { AST } from 'svelte/compiler' */
// @ts-check
import process from 'node:process';
import fs from 'node:fs';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import * as esrap from 'esrap';
import ts from 'esrap/languages/ts';
const DIR = '../../documentation/docs/98-reference/.generated';
@ -97,79 +102,49 @@ function run() {
.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
.replace(/\r\n/g, '\n');
/**
* @type {Array<{
* type: string;
* value: string;
* start: number;
* end: number
* }>}
*/
/** @type {AST.JSComment[]} */
const comments = [];
let ast = acorn.parse(source, {
ecmaVersion: 'latest',
sourceType: 'module',
onComment: (block, value, start, end) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
let b = a;
while (/[ \t]/.test(source[b])) b += 1;
const indentation = source.slice(a, b);
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
let ast = /** @type {ESTree.Node} */ (
/** @type {unknown} */ (
acorn.parse(source, {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true,
onComment: comments
})
)
);
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
comments.forEach((comment) => {
if (comment.type === 'Block') {
comment.value = comment.value.replace(/^\t+/gm, '');
}
});
ast = walk(ast, null, {
_(node, { next }) {
let comment;
while (comments[0] && comments[0].start < node.start) {
comment = comments.shift();
// @ts-expect-error
(node.leadingComments ||= []).push(comment);
}
next();
if (comments[0]) {
const slice = source.slice(node.end, comments[0].start);
if (/^[,) \t]*$/.test(slice)) {
// @ts-expect-error
node.trailingComments = [comments.shift()];
}
}
},
// @ts-expect-error
Identifier(node, context) {
if (node.name === 'CODES') {
return {
/** @type {ESTree.ArrayExpression} */
const array = {
type: 'ArrayExpression',
elements: Object.keys(messages[name]).map((code) => ({
type: 'Literal',
value: code
}))
};
return array;
}
}
});
if (comments.length > 0) {
// @ts-expect-error
(ast.trailingComments ||= []).push(...comments);
}
const body = /** @type {ESTree.Program} */ (ast).body;
const category = messages[name];
// find the `export function CODE` node
const index = ast.body.findIndex((node) => {
const index = body.findIndex((node) => {
if (
node.type === 'ExportNamedDeclaration' &&
node.declaration &&
@ -181,8 +156,19 @@ function run() {
if (index === -1) throw new Error(`missing export function CODE in ${name}.js`);
const template_node = ast.body[index];
ast.body.splice(index, 1);
const template_node = body[index];
body.splice(index, 1);
const jsdoc = /** @type {AST.JSComment} */ (
comments.findLast((comment) => comment.start < /** @type {number} */ (template_node.start))
);
const printed = esrap.print(
/** @type {Node} */ (ast),
ts({
comments: comments.filter((comment) => comment !== jsdoc)
})
);
for (const code in category) {
const { messages } = category[code];
@ -203,7 +189,7 @@ function run() {
};
});
/** @type {import('estree').Expression} */
/** @type {ESTree.Expression} */
let message = { type: 'Literal', value: '' };
let prev_vars;
@ -221,10 +207,10 @@ function run() {
const parts = text.split(/(%\w+%)/);
/** @type {import('estree').Expression[]} */
/** @type {ESTree.Expression[]} */
const expressions = [];
/** @type {import('estree').TemplateElement[]} */
/** @type {ESTree.TemplateElement[]} */
const quasis = [];
for (let i = 0; i < parts.length; i += 1) {
@ -244,7 +230,7 @@ function run() {
}
}
/** @type {import('estree').Expression} */
/** @type {ESTree.Expression} */
const expression = {
type: 'TemplateLiteral',
expressions,
@ -272,138 +258,140 @@ function run() {
prev_vars = vars;
}
const clone = walk(/** @type {import('estree').Node} */ (template_node), null, {
// @ts-expect-error Block is a block comment, which is not recognised
Block(node, context) {
if (!node.value.includes('PARAMETER')) return;
const value = /** @type {string} */ (node.value)
.split('\n')
.map((line) => {
if (line === ' * MESSAGE') {
return messages[messages.length - 1]
.split('\n')
.map((line) => ` * ${line}`)
.join('\n');
}
const clone = /** @type {ESTree.Statement} */ (
walk(/** @type {ESTree.Node} */ (template_node), null, {
FunctionDeclaration(node, context) {
if (node.id.name !== 'CODE') return;
if (line.includes('PARAMETER')) {
return vars
.map((name, i) => {
const optional = i >= group[0].vars.length;
const params = [];
return optional
? ` * @param {string | undefined | null} [${name}]`
: ` * @param {string} ${name}`;
})
.join('\n');
for (const param of node.params) {
if (param.type === 'Identifier' && param.name === 'PARAMETER') {
params.push(...vars.map((name) => ({ type: 'Identifier', name })));
} else {
params.push(param);
}
}
return line;
})
.filter((x) => x !== '')
.join('\n');
return /** @type {ESTree.FunctionDeclaration} */ ({
.../** @type {ESTree.FunctionDeclaration} */ (context.next()),
params,
id: {
...node.id,
name: code
}
});
},
TemplateLiteral(node, context) {
/** @type {ESTree.TemplateElement} */
let quasi = {
type: 'TemplateElement',
value: {
...node.quasis[0].value
},
tail: node.quasis[0].tail
};
if (value !== node.value) {
return { ...node, value };
}
},
FunctionDeclaration(node, context) {
if (node.id.name !== 'CODE') return;
/** @type {ESTree.TemplateLiteral} */
let out = {
type: 'TemplateLiteral',
quasis: [quasi],
expressions: []
};
const params = [];
for (let i = 0; i < node.expressions.length; i += 1) {
const q = structuredClone(node.quasis[i + 1]);
const e = node.expressions[i];
for (const param of node.params) {
if (param.type === 'Identifier' && param.name === 'PARAMETER') {
params.push(...vars.map((name) => ({ type: 'Identifier', name })));
} else {
params.push(param);
}
}
if (e.type === 'Literal' && e.value === 'CODE') {
quasi.value.raw += code + q.value.raw;
continue;
}
return /** @type {import('estree').FunctionDeclaration} */ ({
.../** @type {import('estree').FunctionDeclaration} */ (context.next()),
params,
id: {
...node.id,
name: code
}
});
},
TemplateLiteral(node, context) {
/** @type {import('estree').TemplateElement} */
let quasi = {
type: 'TemplateElement',
value: {
...node.quasis[0].value
},
tail: node.quasis[0].tail
};
if (e.type === 'Identifier' && e.name === 'MESSAGE') {
if (message.type === 'Literal') {
const str = /** @type {string} */ (message.value).replace(/(`|\${)/g, '\\$1');
quasi.value.raw += str + q.value.raw;
continue;
}
if (message.type === 'TemplateLiteral') {
const m = structuredClone(message);
quasi.value.raw += m.quasis[0].value.raw;
out.quasis.push(...m.quasis.slice(1));
out.expressions.push(...m.expressions);
quasi = m.quasis[m.quasis.length - 1];
quasi.value.raw += q.value.raw;
continue;
}
}
/** @type {import('estree').TemplateLiteral} */
let out = {
type: 'TemplateLiteral',
quasis: [quasi],
expressions: []
};
out.quasis.push((quasi = q));
out.expressions.push(/** @type {ESTree.Expression} */ (context.visit(e)));
}
for (let i = 0; i < node.expressions.length; i += 1) {
const q = structuredClone(node.quasis[i + 1]);
const e = node.expressions[i];
return out;
},
Literal(node) {
if (node.value === 'CODE') {
return {
type: 'Literal',
value: code
};
}
},
Identifier(node) {
if (node.name !== 'MESSAGE') return;
return message;
}
})
);
if (e.type === 'Literal' && e.value === 'CODE') {
quasi.value.raw += code + q.value.raw;
continue;
const jsdoc_clone = {
...jsdoc,
value: /** @type {string} */ (jsdoc.value)
.split('\n')
.map((line) => {
if (line === ' * MESSAGE') {
return messages[messages.length - 1]
.split('\n')
.map((line) => ` * ${line}`)
.join('\n');
}
if (e.type === 'Identifier' && e.name === 'MESSAGE') {
if (message.type === 'Literal') {
const str = /** @type {string} */ (message.value).replace(/(`|\${)/g, '\\$1');
quasi.value.raw += str + q.value.raw;
continue;
}
if (line.includes('PARAMETER')) {
return vars
.map((name, i) => {
const optional = i >= group[0].vars.length;
if (message.type === 'TemplateLiteral') {
const m = structuredClone(message);
quasi.value.raw += m.quasis[0].value.raw;
out.quasis.push(...m.quasis.slice(1));
out.expressions.push(...m.expressions);
quasi = m.quasis[m.quasis.length - 1];
quasi.value.raw += q.value.raw;
continue;
}
return optional
? ` * @param {string | undefined | null} [${name}]`
: ` * @param {string} ${name}`;
})
.join('\n');
}
out.quasis.push((quasi = q));
out.expressions.push(/** @type {import('estree').Expression} */ (context.visit(e)));
}
return line;
})
.filter((x) => x !== '')
.join('\n')
};
return out;
},
Literal(node) {
if (node.value === 'CODE') {
return {
type: 'Literal',
value: code
};
}
},
Identifier(node) {
if (node.name !== 'MESSAGE') return;
return message;
}
});
const block = esrap.print(
// @ts-expect-error some bullshit
/** @type {ESTree.Program} */ ({ ...ast, body: [clone] }),
ts({ comments: [jsdoc_clone] })
).code;
// @ts-expect-error
ast.body.push(clone);
}
printed.code += `\n\n${block}`;
const module = esrap.print(ast);
body.push(clone);
}
fs.writeFileSync(
dest,
`/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
module.code,
printed.code,
'utf-8'
);
}

@ -15,10 +15,12 @@ class InternalCompileError extends Error {
constructor(code, message, position) {
super(message);
this.stack = ''; // avoid unnecessary noise; don't set it as a class property or it becomes enumerable
// We want to extend from Error so that various bundler plugins properly handle it.
// But we also want to share the same object shape with that of warnings, therefore
// we create an instance of the shared class an copy over its properties.
this.#diagnostic = new CompileDiagnostic(code, message, position);
Object.assign(this, this.#diagnostic);
this.name = 'CompileError';
}
@ -816,7 +818,9 @@ export function bind_invalid_expression(node) {
* @returns {never}
*/
export function bind_invalid_name(node, name, explanation) {
e(node, 'bind_invalid_name', `${explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`);
e(node, 'bind_invalid_name', `${explanation
? `\`bind:${name}\` is not a valid binding. ${explanation}`
: `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`);
}
/**

@ -3,7 +3,6 @@
/** @import { AST } from './public.js' */
import { walk as zimmerframe_walk } from 'zimmerframe';
import { convert } from './legacy.js';
import { parse as parse_acorn } from './phases/1-parse/acorn.js';
import { parse as _parse } from './phases/1-parse/index.js';
import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js';
import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
@ -21,9 +20,8 @@ export { default as preprocess } from './preprocess/index.js';
*/
export function compile(source, options) {
source = remove_bom(source);
state.reset_warning_filter(options.warningFilter);
state.reset_warnings(options.warningFilter);
const validated = validate_component_options(options, '');
state.reset(source, validated);
let parsed = _parse(source);
@ -65,11 +63,10 @@ export function compile(source, options) {
*/
export function compileModule(source, options) {
source = remove_bom(source);
state.reset_warning_filter(options.warningFilter);
state.reset_warnings(options.warningFilter);
const validated = validate_module_options(options, '');
state.reset(source, validated);
const analysis = analyze_module(parse_acorn(source, false), validated);
const analysis = analyze_module(source, validated);
return transform_module(analysis, source, validated);
}
@ -97,6 +94,7 @@ export function compileModule(source, options) {
* @returns {Record<string, any>}
*/
// TODO 6.0 remove unused `filename`
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
@ -105,14 +103,15 @@ export function compileModule(source, options) {
*
* The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile.
*
* The `filename` option is unused and will be removed in Svelte 6.0.
*
* @param {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
* @returns {AST.Root | LegacyRoot}
*/
export function parse(source, { filename, rootDir, modern, loose } = {}) {
export function parse(source, { modern, loose } = {}) {
source = remove_bom(source);
state.reset_warning_filter(() => false);
state.reset(source, { filename: filename ?? '(unknown)', rootDir });
state.reset_warnings(() => false);
const ast = _parse(source, loose);
return to_public_ast(source, ast, modern);

@ -451,6 +451,7 @@ export function convert(source, ast) {
SpreadAttribute(node) {
return { ...node, type: 'Spread' };
},
// @ts-ignore
StyleSheet(node, context) {
return {
...node,

@ -9,7 +9,7 @@ import { parse } from '../phases/1-parse/index.js';
import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
import { analyze_component } from '../phases/2-analyze/index.js';
import { get_rune } from '../phases/scope.js';
import { reset, reset_warning_filter } from '../state.js';
import { reset, reset_warnings } from '../state.js';
import {
extract_identifiers,
extract_all_identifiers_from_expression,
@ -134,8 +134,7 @@ export function migrate(source, { filename, use_ts } = {}) {
return start + style_placeholder + end;
});
reset_warning_filter(() => false);
reset(source, { filename: filename ?? '(unknown)' });
reset_warnings(() => false);
let parsed = parse(source);

@ -1,18 +1,32 @@
/** @import { Comment, Program } from 'estree' */
/** @import { AST } from '#compiler' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin());
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/**
* @param {string} source
* @param {AST.JSComment[]} comments
* @param {boolean} typescript
* @param {boolean} [is_script]
*/
export function parse(source, typescript, is_script) {
export function parse(source, comments, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
@ -53,13 +67,19 @@ export function parse(source, typescript, is_script) {
/**
* @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(source, typescript, index) {
export function parse_expression_at(source, comments, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments),
index
);
const ast = parser.parseExpressionAt(source, index, {
onComment,
@ -78,26 +98,20 @@ export function parse_expression_at(source, typescript, index) {
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source
* @param {CommentWithLocation[]} comments
* @param {number} index
*/
function get_comment_handlers(source) {
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** @type {CommentWithLocation[]} */
const comments = [];
function get_comment_handlers(source, comments, index = 0) {
return {
/**
* @param {boolean} block
* @param {string} value
* @param {number} start
* @param {number} end
* @param {import('acorn').Position} [start_loc]
* @param {import('acorn').Position} [end_loc]
*/
onComment: (block, value, start, end) => {
onComment: (block, value, start, end, start_loc, end_loc) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
@ -109,13 +123,26 @@ function get_comment_handlers(source) {
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
comments.push({
type: block ? 'Block' : 'Line',
value,
start,
end,
loc: {
start: /** @type {import('acorn').Position} */ (start_loc),
end: /** @type {import('acorn').Position} */ (end_loc)
}
});
},
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) {
if (comments.length === 0) return;
comments = comments
.filter((comment) => comment.start >= index)
.map(({ type, value, start, end }) => ({ type, value, start, end }));
walk(ast, null, {
_(node, { next, path }) {
let comment;

@ -1,4 +1,5 @@
/** @import { AST } from '#compiler' */
/** @import { Comment } from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
@ -8,6 +9,7 @@ import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
@ -87,6 +89,7 @@ export class Parser {
type: 'Root',
fragment: create_fragment(),
options: null,
comments: [],
metadata: {
ts: this.ts
}
@ -299,6 +302,8 @@ export class Parser {
* @returns {AST.Root}
*/
export function parse(template, loose = false) {
state.set_source(template);
const parser = new Parser(template, loose);
return parser.root;
}

@ -59,7 +59,12 @@ export default function read_pattern(parser) {
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ (
parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1)
parse_expression_at(
`${space_with_newline}(${pattern_string} = 1)`,
parser.root.comments,
parser.ts,
start - 1
)
).left;
expression.typeAnnotation = read_type_annotation(parser);
@ -96,13 +101,13 @@ function read_type_annotation(parser) {
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.ts, a);
let expression = parse_expression_at(template, parser.root.comments, parser.ts, a);
// `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
expression = parse_expression_at(template.slice(0, b), parser.ts, a);
expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a);
}
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that

@ -34,12 +34,24 @@ export function get_loose_identifier(parser, opening_token) {
*/
export default function read_expression(parser, opening_token, disallow_loose) {
try {
const node = parse_expression_at(parser.template, parser.ts, parser.index);
let comment_index = parser.root.comments.length;
const node = parse_expression_at(
parser.template,
parser.root.comments,
parser.ts,
parser.index
);
let num_parens = 0;
if (node.leadingComments !== undefined && node.leadingComments.length > 0) {
parser.index = node.leadingComments.at(-1).end;
let i = parser.root.comments.length;
while (i-- > comment_index) {
const comment = parser.root.comments[i];
if (comment.end < node.start) {
parser.index = comment.end;
break;
}
}
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
@ -47,9 +59,9 @@ export default function read_expression(parser, opening_token, disallow_loose) {
}
let index = /** @type {number} */ (node.end);
if (node.trailingComments !== undefined && node.trailingComments.length > 0) {
index = node.trailingComments.at(-1).end;
}
const last_comment = parser.root.comments.at(-1);
if (last_comment && last_comment.end > index) index = last_comment.end;
while (num_parens > 0) {
const char = parser.template[index];

@ -34,7 +34,7 @@ export function read_script(parser, start, attributes) {
let ast;
try {
ast = acorn.parse(source, parser.ts, true);
ast = acorn.parse(source, parser.root.comments, parser.ts, true);
} catch (err) {
parser.acorn_error(err);
}

@ -398,7 +398,12 @@ function open(parser) {
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start)
parse_expression_at(
prelude + `${params} => {}`,
parser.root.comments,
parser.ts,
params_start
)
)
: { params: [] };

@ -1,8 +1,9 @@
/** @import { Expression, Node, Program } from 'estree' */
/** @import { Comment, Expression, Node, Program } from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { extract_identifiers } from '../../utils/ast.js';
@ -75,6 +76,7 @@ import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclarator } from './visitors/VariableDeclarator.js';
import is_reference from 'is-reference';
import { mark_subtree_dynamic } from './visitors/shared/fragment.js';
import * as state from '../../state.js';
/**
* @type {Visitors}
@ -231,11 +233,17 @@ function get_component_name(filename) {
const RESERVED = ['$$props', '$$restProps', '$$slots'];
/**
* @param {Program} ast
* @param {string} source
* @param {ValidatedModuleCompileOptions} options
* @returns {Analysis}
*/
export function analyze_module(ast, options) {
export function analyze_module(source, options) {
/** @type {AST.JSComment[]} */
const comments = [];
state.set_source(source);
const ast = parse(source, comments, false, false);
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null);
for (const [name, references] of scope.references) {
@ -259,9 +267,17 @@ export function analyze_module(ast, options) {
runes: true,
immutable: true,
tracing: false,
comments,
classes: new Map()
};
state.reset({
dev: options.dev,
filename: options.filename,
rootDir: options.rootDir,
runes: true
});
walk(
/** @type {Node} */ (ast),
{
@ -429,6 +445,7 @@ export function analyze_component(root, source, options) {
module,
instance,
template,
comments: root.comments,
elements: [],
runes,
// if we are not in runes mode but we have no reserved references ($$props, $$restProps)
@ -498,6 +515,14 @@ export function analyze_component(root, source, options) {
snippets: new Set()
};
state.reset({
component_name: analysis.name,
dev: options.dev,
filename: options.filename,
rootDir: options.rootDir,
runes: true
});
if (!runes) {
// every exported `let` or `var` declaration becomes a prop, everything else becomes an export
for (const node of instance.ast.body) {

@ -168,9 +168,9 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
update: /** @type {any} */ (null),
expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null)
template: /** @type {any} */ (null),
memoizer: /** @type {any} */ (null)
};
const module = /** @type {ESTree.Program} */ (
@ -362,6 +362,9 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (template.body)
]);
// trick esrap into including comments
component_block.loc = instance.loc;
if (!analysis.runes) {
// Bind static exports to props so that people can access them with bind:x
for (const { name, alias } of analysis.exports) {

@ -12,6 +12,7 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { Template } from './transform-template/template.js';
import type { Memoizer } from './visitors/shared/utils.js';
export interface ClientTransformState extends TransformState {
/**
@ -49,8 +50,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** Expressions used inside the render effect */
readonly expressions: Expression[];
/** Memoized expressions */
readonly memoizer: Memoizer;
/** The HTML template string */
readonly template: Template;
readonly metadata: {

@ -137,6 +137,7 @@ function build_assignment(operator, left, right, context) {
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'derived' &&
binding.kind !== 'store_sub' &&
context.state.analysis.runes &&
should_proxy(right, context.state.scope) &&

@ -5,7 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@ -54,7 +54,7 @@ export function AwaitBlock(node, context) {
}
context.state.init.push(
b.stmt(
add_svelte_meta(
b.call(
'$.await',
context.state.node,
@ -64,7 +64,9 @@ export function AwaitBlock(node, context) {
: b.null,
then_block,
catch_block
)
),
node,
'await'
)
);
}

@ -13,7 +13,7 @@ import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -337,7 +337,7 @@ export function EachBlock(node, context) {
);
}
context.state.init.push(b.stmt(b.call('$.each', ...args)));
context.state.init.push(add_svelte_meta(b.call('$.each', ...args), node, 'each'));
}
/**

@ -6,7 +6,7 @@ import * as b from '#compiler/builders';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement } from './shared/utils.js';
import { build_render_statement, Memoizer } from './shared/utils.js';
import { Template } from '../transform-template/template.js';
/**
@ -64,8 +64,8 @@ export function Fragment(node, context) {
...context.state,
init: [],
update: [],
expressions: [],
after_update: [],
memoizer: new Memoizer(),
template: new Template(),
transform: { ...context.state.transform },
metadata: {

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@ -74,7 +74,7 @@ export function IfBlock(node, context) {
args.push(b.id('$$elseif'));
}
statements.push(b.stmt(b.call('$.if', ...args)));
statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
context.state.init.push(b.block(statements));
}

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
* @param {AST.KeyBlock} node
@ -15,6 +15,10 @@ export function KeyBlock(node, context) {
const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push(
b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)))
add_svelte_meta(
b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)),
node,
'key'
)
);
}

@ -22,13 +22,7 @@ import {
build_set_style
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import {
build_render_statement,
build_template_chunk,
build_update_assignment,
get_expression_id,
memoize_expression
} from './shared/utils.js';
import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
/**
@ -200,16 +194,16 @@ export function RegularElement(node, context) {
const node_id = context.state.node;
/** If true, needs `__value` for inputs */
const needs_special_value_handling =
node.name === 'option' ||
node.name === 'select' ||
bindings.has('group') ||
bindings.has('checked');
if (has_spread) {
build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id);
} else {
/** If true, needs `__value` for inputs */
const needs_special_value_handling =
node.name === 'option' ||
node.name === 'select' ||
bindings.has('group') ||
bindings.has('checked');
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (is_event_attribute(attribute)) {
visit_event_attribute(attribute, context);
@ -217,7 +211,6 @@ export function RegularElement(node, context) {
}
if (needs_special_value_handling && attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
continue;
}
@ -260,8 +253,7 @@ export function RegularElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) =>
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
(value, metadata) => (metadata.has_call ? context.state.memoizer.add(value) : value)
);
const update = build_element_attribute_update(node, node_id, name, value, attributes);
@ -392,6 +384,15 @@ export function RegularElement(node, context) {
context.state.update.push(b.stmt(b.assignment('=', dir, dir)));
}
if (!has_spread && needs_special_value_handling) {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
break;
}
}
}
context.state.template.pop_element();
}
@ -453,11 +454,15 @@ function setup_select_synchronization(value_binding, context) {
/**
* @param {AST.ClassDirective[]} class_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context
* @param {Memoizer} memoizer
* @return {ObjectExpression | Identifier}
*/
export function build_class_directives_object(class_directives, expressions, context) {
export function build_class_directives_object(
class_directives,
context,
memoizer = context.state.memoizer
) {
let properties = [];
let has_call_or_state = false;
@ -469,38 +474,40 @@ export function build_class_directives_object(class_directives, expressions, con
const directives = b.object(properties);
return has_call_or_state ? get_expression_id(expressions, directives) : directives;
return has_call_or_state ? memoizer.add(directives) : directives;
}
/**
* @param {AST.StyleDirective[]} style_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context
* @return {ObjectExpression | ArrayExpression}}
* @param {Memoizer} memoizer
* @return {ObjectExpression | ArrayExpression | Identifier}}
*/
export function build_style_directives_object(style_directives, expressions, context) {
let normal_properties = [];
let important_properties = [];
export function build_style_directives_object(
style_directives,
context,
memoizer = context.state.memoizer
) {
const normal = b.object([]);
const important = b.object([]);
for (const directive of style_directives) {
let has_call_or_state = false;
for (const d of style_directives) {
const expression =
directive.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
: build_attribute_value(directive.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(expressions, value) : value
).value;
const property = b.init(directive.name, expression);
if (directive.modifiers.includes('important')) {
important_properties.push(property);
} else {
normal_properties.push(property);
}
d.value === true
? build_getter(b.id(d.name), context.state)
: build_attribute_value(d.value, context).value;
const object = d.modifiers.includes('important') ? important : normal;
object.properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
}
return important_properties.length
? b.array([b.object(normal_properties), b.object(important_properties)])
: b.object(normal_properties);
const directives = important.properties.length ? b.array([normal, important]) : normal;
return has_call_or_state ? memoizer.add(directives) : directives;
}
/**
@ -622,12 +629,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call
? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately
is_select_with_value
? memoize_expression(state, value)
: get_expression_id(state.expressions, value)
: value
metadata.has_call ? state.memoizer.add(value) : value
);
const evaluated = context.state.scope.evaluate(value);
@ -652,23 +654,21 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
: inner_assignment
);
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value))));
}
if (has_state) {
const id = state.scope.generate(`${node_id.name}_value`);
build_update_assignment(
state,
id,
// `<option>` is a special case: The value property reflects to the DOM. If the value is set to undefined,
// that means the value should be set to the empty string. To be able to do that when the value is
// initially undefined, we need to set a value that is guaranteed to be different.
element === 'option' ? b.object([]) : undefined,
value,
update
);
const id = b.id(state.scope.generate(`${node_id.name}_value`));
// `<option>` is a special case: The value property reflects to the DOM. If the value is set to undefined,
// that means the value should be set to the empty string. To be able to do that when the value is
// initially undefined, we need to set a value that is guaranteed to be different.
const init = element === 'option' ? b.object([]) : undefined;
state.init.push(b.var(id, init));
state.update.push(b.if(b.binary('!==', id, b.assignment('=', id, value)), b.block([update])));
} else {
state.init.push(update);
}
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id)));
}
}

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
import { add_svelte_meta, build_expression } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -48,16 +48,22 @@ export function RenderTag(node, context) {
}
context.state.init.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
add_svelte_meta(
b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args),
node,
'render'
)
);
} else {
context.state.init.push(
b.stmt(
add_svelte_meta(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
context.state.node,
...args
)
),
node,
'render'
)
);
}

@ -10,7 +10,7 @@ import {
build_attribute_effect,
build_set_class
} from './shared/element.js';
import { build_render_statement } from './shared/utils.js';
import { build_render_statement, Memoizer } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -46,8 +46,8 @@ export function SvelteElement(node, context) {
node: element_id,
init: [],
update: [],
expressions: [],
after_update: []
after_update: [],
memoizer: new Memoizer()
}
};

@ -4,7 +4,12 @@
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
import {
build_bind_this,
memoize_expression,
validate_binding,
add_svelte_meta
} from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js';
@ -483,7 +488,8 @@ export function build_component(node, component_name, context) {
);
} else {
context.state.template.push_comment();
statements.push(b.stmt(fn(anchor)));
statements.push(add_svelte_meta(fn(anchor), node, 'component', { componentTag: node.name }));
}
return statements.length > 1 ? b.block(statements) : statements[0];

@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_expression, build_template_chunk, get_expression_id } from './utils.js';
import { build_expression, build_template_chunk, Memoizer } from './utils.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@ -28,18 +28,12 @@ export function build_attribute_effect(
/** @type {ObjectExpression['properties']} */
const values = [];
/** @type {Expression[]} */
const expressions = [];
/** @param {Expression} value */
function memoize(value) {
return b.id(`$${expressions.push(value) - 1}`);
}
const memoizer = new Memoizer();
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? memoize(value) : value
metadata.has_call ? memoizer.add(value) : value
);
if (
@ -57,7 +51,7 @@ export function build_attribute_effect(
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call) {
value = memoize(value);
value = memoizer.add(value);
}
values.push(b.spread(value));
@ -69,7 +63,7 @@ export function build_attribute_effect(
b.prop(
'init',
b.array([b.id('$.CLASS')]),
build_class_directives_object(class_directives, expressions, context)
build_class_directives_object(class_directives, context, memoizer)
)
);
}
@ -79,21 +73,20 @@ export function build_attribute_effect(
b.prop(
'init',
b.array([b.id('$.STYLE')]),
build_style_directives_object(style_directives, expressions, context)
build_style_directives_object(style_directives, context, memoizer)
)
);
}
const ids = memoizer.apply();
context.state.init.push(
b.stmt(
b.call(
'$.attribute_effect',
element_id,
b.arrow(
expressions.map((_, i) => b.id(`$${i}`)),
b.object(values)
),
expressions.length > 0 && b.array(expressions.map((expression) => b.thunk(expression))),
b.arrow(ids, b.object(values)),
memoizer.sync_values(),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
@ -158,7 +151,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
value = b.call('$.clsx', value);
}
return metadata.has_call ? get_expression_id(context.state.expressions, value) : value;
return metadata.has_call ? context.state.memoizer.add(value) : value;
});
/** @type {Identifier | undefined} */
@ -171,7 +164,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
let next;
if (class_directives.length) {
next = build_class_directives_object(class_directives, context.state.expressions, context);
next = build_class_directives_object(class_directives, context);
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {
@ -226,7 +219,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
*/
export function build_set_style(node_id, attribute, style_directives, context) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
metadata.has_call ? context.state.memoizer.add(value) : value
);
/** @type {Identifier | undefined} */
@ -235,11 +228,11 @@ export function build_set_style(node_id, attribute, style_directives, context) {
/** @type {ObjectExpression | Identifier | undefined} */
let prev;
/** @type {ArrayExpression | ObjectExpression | undefined} */
/** @type {Expression | undefined} */
let next;
if (style_directives.length) {
next = build_style_directives_object(style_directives, context.state.expressions, context);
next = build_style_directives_object(style_directives, context);
has_state ||= style_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {

@ -1,4 +1,4 @@
/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe';
@ -7,7 +7,7 @@ import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator } from '../../../../../state.js';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter, create_derived } from '../../utils.js';
/**
@ -21,12 +21,41 @@ export function memoize_expression(state, value) {
}
/**
* Pushes `value` into `expressions` and returns a new id
* @param {Expression[]} expressions
* @param {Expression} value
* A utility for extracting complex expressions (such as call expressions)
* from templates and replacing them with `$0`, `$1` etc
*/
export function get_expression_id(expressions, value) {
return b.id(`$${expressions.push(value) - 1}`);
export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#sync = [];
/**
* @param {Expression} expression
*/
add(expression) {
const id = b.id('#'); // filled in later
this.#sync.push({ id, expression });
return id;
}
apply() {
return this.#sync.map((memo, i) => {
memo.id.name = `$${i}`;
return memo.id;
});
}
deriveds(runes = true) {
return this.#sync.map((memo) =>
b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression)))
);
}
sync_values() {
if (this.#sync.length === 0) return;
return b.array(this.#sync.map((memo) => b.thunk(memo.expression)));
}
}
/**
@ -40,8 +69,7 @@ export function build_template_chunk(
values,
context,
state = context.state,
memoize = (value, metadata) =>
metadata.has_call ? get_expression_id(state.expressions, value) : value
memoize = (value, metadata) => (metadata.has_call ? state.memoizer.add(value) : value)
) {
/** @type {Expression[]} */
const expressions = [];
@ -128,18 +156,20 @@ export function build_template_chunk(
* @param {ComponentClientTransformState} state
*/
export function build_render_statement(state) {
const ids = state.memoizer.apply();
const values = state.memoizer.sync_values();
return b.stmt(
b.call(
'$.template_effect',
b.arrow(
state.expressions.map((_, i) => b.id(`$${i}`)),
ids,
state.update.length === 1 && state.update[0].type === 'ExpressionStatement'
? state.update[0].expression
: b.block(state.update)
),
state.expressions.length > 0 &&
b.array(state.expressions.map((expression) => b.thunk(expression))),
state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal')
values,
values && !state.analysis.runes && b.id('$.derived_safe_equal')
)
);
}
@ -165,20 +195,6 @@ export function parse_directive_name(name) {
return expression;
}
/**
* @param {ComponentClientTransformState} state
* @param {string} id
* @param {Expression | undefined} init
* @param {Expression} value
* @param {ExpressionStatement} update
*/
export function build_update_assignment(state, id, init, value, update) {
state.init.push(b.var(id, init));
state.update.push(
b.if(b.binary('!==', b.id(id), b.assignment('=', b.id(id), value)), b.block([update]))
);
}
/**
* Serializes `bind:this` for components and elements.
* @param {Identifier | MemberExpression | SequenceExpression} expression
@ -408,3 +424,34 @@ export function build_expression(context, expression, metadata, state = context.
return sequence;
}
/**
* Wraps a statement/expression with dev stack tracking in dev mode
* @param {Expression} expression - The function call to wrap (e.g., $.if, $.each, etc.)
* @param {{ start?: number }} node - AST node for location info
* @param {'component' | 'if' | 'each' | 'await' | 'key' | 'render'} type - Type of block/component
* @param {Record<string, number | string>} [additional] - Any additional properties to add to the dev stack entry
* @returns {ExpressionStatement} - Statement with or without dev stack wrapping
*/
export function add_svelte_meta(expression, node, type, additional) {
if (!dev) {
return b.stmt(expression);
}
const location = node.start !== undefined && locator(node.start);
if (!location) {
return b.stmt(expression);
}
return b.stmt(
b.call(
'$.add_svelte_meta',
b.arrow([], expression),
b.literal(type),
b.id(component_name),
b.literal(location.line),
b.literal(location.column),
additional && b.object(Object.entries(additional).map(([k, v]) => b.init(k, b.literal(v))))
)
);
}

@ -1,6 +1,8 @@
/** @import { Node } from 'esrap/languages/ts' */
/** @import { ValidatedCompileOptions, CompileResult, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentAnalysis, Analysis } from '../types' */
import { print } from 'esrap';
import ts from 'esrap/languages/ts';
import { VERSION } from '../../../version.js';
import { server_component, server_module } from './server/transform-server.js';
import { client_component, client_module } from './client/transform-client.js';
@ -34,7 +36,7 @@ export function transform_component(analysis, source, options) {
const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte');
const js = print(program, {
const js = print(/** @type {Node} */ (program), ts({ comments: analysis.comments }), {
// include source content; makes it easier/more robust looking up the source map code
// (else esrap does return null for source and sourceMapContent which may trip up tooling)
sourceMapContent: source,
@ -93,13 +95,19 @@ export function transform_module(analysis, source, options) {
];
}
const js = print(/** @type {Node} */ (program), ts({ comments: analysis.comments }), {
// include source content; makes it easier/more robust looking up the source map code
// (else esrap does return null for source and sourceMapContent which may trip up tooling)
sourceMapContent: source,
sourceMapSource: get_source_name(options.filename, undefined, 'input.svelte.js')
});
// prepend comment
js.code = `/* ${basename} generated by Svelte v${VERSION} */\n${js.code}`;
js.map.mappings = ';' + js.map.mappings;
return {
js: print(program, {
// include source content; makes it easier/more robust looking up the source map code
// (else esrap does return null for source and sourceMapContent which may trip up tooling)
sourceMapContent: source,
sourceMapSource: get_source_name(options.filename, undefined, 'input.svelte.js')
}),
js,
css: null,
metadata: {
runes: true

@ -242,6 +242,9 @@ export function server_component(analysis, options) {
.../** @type {Statement[]} */ (template.body)
]);
// trick esrap into including comments
component_block.loc = instance.loc;
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(

@ -2,6 +2,7 @@ import type { AST, Binding, StateField } from '#compiler';
import type {
AssignmentExpression,
ClassBody,
Comment,
Identifier,
LabeledStatement,
Node,
@ -33,10 +34,13 @@ export interface ReactiveStatement {
*/
export interface Analysis {
module: Js;
/** @deprecated use `component_name` from `state.js` instead */
name: string; // TODO should this be filename? it's used in `compileModule` as well as `compile`
/** @deprecated use `runes` from `state.js` instead */
runes: boolean;
immutable: boolean;
tracing: boolean;
comments: AST.JSComment[];
classes: Map<ClassBody, Map<string, StateField>>;
@ -88,6 +92,7 @@ export interface ComponentAnalysis extends Analysis {
keyframes: string[];
has_global: boolean;
};
/** @deprecated use `source` from `state.js` instead */
source: string;
undefined_exports: Map<string, Node>;
/**

@ -16,6 +16,11 @@ export let warnings = [];
*/
export let filename;
/**
* The name of the component that is used in the `export default function ...` statement.
*/
export let component_name = '<unknown>';
/**
* The original source code
* @type {string}
@ -28,8 +33,16 @@ export let source;
*/
export let dev;
export let runes = false;
export let locator = getLocator('', { offsetLine: 1 });
/** @param {string} value */
export function set_source(value) {
source = value;
locator = getLocator(source, { offsetLine: 1 });
}
/**
* @param {AST.SvelteNode & { start?: number | undefined }} node
*/
@ -71,8 +84,9 @@ export function pop_ignore() {
*
* @param {(warning: Warning) => boolean} fn
*/
export function reset_warning_filter(fn = () => true) {
export function reset_warnings(fn = () => true) {
warning_filter = fn;
warnings = [];
}
/**
@ -85,23 +99,27 @@ export function is_ignored(node, code) {
}
/**
* @param {string} _source
* @param {{ dev?: boolean; filename: string; rootDir?: string }} options
* @param {{
* dev: boolean;
* filename: string;
* component_name?: string;
* rootDir?: string;
* runes: boolean;
* }} state
*/
export function reset(_source, options) {
source = _source;
const root_dir = options.rootDir?.replace(/\\/g, '/');
filename = options.filename.replace(/\\/g, '/');
export function reset(state) {
const root_dir = state.rootDir?.replace(/\\/g, '/');
filename = state.filename.replace(/\\/g, '/');
dev = !!options.dev;
dev = state.dev;
runes = state.runes;
component_name = state.component_name ?? '(unknown)';
if (typeof root_dir === 'string' && filename.startsWith(root_dir)) {
// make filename relative to rootDir
filename = filename.replace(root_dir, '').replace(/^[/\\]/, '');
}
locator = getLocator(source, { offsetLine: 1 });
warnings = [];
ignore_stack = [];
ignore_map.clear();
}

@ -72,6 +72,8 @@ export namespace AST {
instance: Script | null;
/** The parsed `<script module>` element, if exists */
module: Script | null;
/** Comments found in <script> and {expressions} */
comments: JSComment[];
/** @internal */
metadata: {
/** Whether the component was parsed with typescript */
@ -537,6 +539,17 @@ export namespace AST {
attributes: Attribute[];
}
export interface JSComment {
type: 'Line' | 'Block';
value: string;
start: number;
end: number;
loc: {
start: { line: number; column: number };
end: { line: number; column: number };
};
}
export type AttributeLike = Attribute | SpreadAttribute | Directive;
export type Directive =
@ -593,7 +606,7 @@ export namespace AST {
| AST.Comment
| Block;
export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node;
export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node | Script;
export type { _CSS as CSS };
}

@ -398,10 +398,13 @@ export function merge_with_preprocessor_map(result, options, source_name) {
// map may contain a different file name. Patch our map beforehand to align sources so merging
// with the preprocessor map works correctly.
result.map.sources = [file_basename];
result.map = apply_preprocessor_sourcemap(
file_basename,
Object.assign(
result.map,
/** @type {any} */ (options.sourcemap)
apply_preprocessor_sourcemap(
file_basename,
result.map,
/** @type {any} */ (options.sourcemap)
)
);
// After applying the preprocessor map, we need to do the inverse and make the sources
// relative to the input file again in case the output code is in a different directory.

@ -1,12 +1,6 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
import {
warnings,
ignore_stack,
ignore_map,
warning_filter
} from './state.js';
import { warnings, ignore_stack, ignore_map, warning_filter } from './state.js';
import { CompileDiagnostic } from './utils/compile_diagnostic.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */
@ -40,6 +34,7 @@ function w(node, code, message) {
const warning = new InternalCompileWarning(code, message, node && node.start !== undefined ? [node.start, node.end ?? node.start] : undefined);
if (!warning_filter(warning)) return;
warnings.push(warning);
}
@ -496,7 +491,9 @@ export function a11y_role_supports_aria_props_implicit(node, attribute, role, na
* @param {string | undefined | null} [suggestion]
*/
export function a11y_unknown_aria_attribute(node, attribute, suggestion) {
w(node, 'a11y_unknown_aria_attribute', `${suggestion ? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?` : `Unknown aria attribute 'aria-${attribute}'`}\nhttps://svelte.dev/e/a11y_unknown_aria_attribute`);
w(node, 'a11y_unknown_aria_attribute', `${suggestion
? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?`
: `Unknown aria attribute 'aria-${attribute}'`}\nhttps://svelte.dev/e/a11y_unknown_aria_attribute`);
}
/**
@ -506,7 +503,9 @@ export function a11y_unknown_aria_attribute(node, attribute, suggestion) {
* @param {string | undefined | null} [suggestion]
*/
export function a11y_unknown_role(node, role, suggestion) {
w(node, 'a11y_unknown_role', `${suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`);
w(node, 'a11y_unknown_role', `${suggestion
? `Unknown role '${role}'. Did you mean '${suggestion}'?`
: `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`);
}
/**
@ -534,7 +533,9 @@ export function legacy_code(node, code, suggestion) {
* @param {string | undefined | null} [suggestion]
*/
export function unknown_code(node, code, suggestion) {
w(node, 'unknown_code', `${suggestion ? `\`${code}\` is not a recognised code (did you mean \`${suggestion}\`?)` : `\`${code}\` is not a recognised code`}\nhttps://svelte.dev/e/unknown_code`);
w(node, 'unknown_code', `${suggestion
? `\`${code}\` is not a recognised code (did you mean \`${suggestion}\`?)`
: `\`${code}\` is not a recognised code`}\nhttps://svelte.dev/e/unknown_code`);
}
/**

@ -1,7 +1,7 @@
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.js' */
import { untrack } from './internal/client/runtime.js';
import { active_reaction, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
@ -44,6 +44,37 @@ if (DEV) {
throw_rune_error('$bindable');
}
/**
* Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed.
*
* Must be called while a derived or effect is running.
*
* ```svelte
* <script>
* import { getAbortSignal } from 'svelte';
*
* let { id } = $props();
*
* async function getData(id) {
* const response = await fetch(`/items/${id}`, {
* signal: getAbortSignal()
* });
*
* return await response.json();
* }
*
* const data = $derived(await getData(id));
* </script>
* ```
*/
export function getAbortSignal() {
if (active_reaction === null) {
e.get_abort_signal_outside_reaction();
}
return (active_reaction.ac ??= new AbortController()).signal;
}
/**
* `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM.
* Unlike `$effect`, the provided function only runs once.

@ -35,6 +35,8 @@ export function unmount() {
export async function tick() {}
export { getAbortSignal } from './internal/server/abort-signal.js';
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
export { createRawSnippet } from './internal/server/blocks/snippet.js';

@ -15,18 +15,22 @@ export const DESTROYED = 1 << 14;
export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 16;
/** 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 << 17;
export const INSPECT_EFFECT = 1 << 18;
export const HEAD_EFFECT = 1 << 19;
export const EFFECT_HAS_DERIVED = 1 << 20;
export const EFFECT_IS_UPDATING = 1 << 21;
export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const EFFECT_PRESERVED = 1 << 19;
export const EFFECT_IS_UPDATING = 1 << 20;
export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
/** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */
export const STALE_REACTION = new (class StaleReactionError extends Error {
name = 'StaleReactionError';
message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';
})();
export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const COMMENT_NODE = 8;

@ -1,4 +1,4 @@
/** @import { ComponentContext } from '#client' */
/** @import { ComponentContext, DevStackEntry } from '#client' */
import { DEV } from 'esm-env';
import { lifecycle_outside_component } from '../shared/errors.js';
@ -11,6 +11,7 @@ import {
} from './runtime.js';
import { effect, teardown } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js';
/** @type {ComponentContext | null} */
export let component_context = null;
@ -20,6 +21,43 @@ export function set_component_context(context) {
component_context = context;
}
/** @type {DevStackEntry | null} */
export let dev_stack = null;
/** @param {DevStackEntry | null} stack */
export function set_dev_stack(stack) {
dev_stack = stack;
}
/**
* Execute a callback with a new dev stack entry
* @param {() => any} callback - Function to execute
* @param {DevStackEntry['type']} type - Type of block/component
* @param {any} component - Component function
* @param {number} line - Line number
* @param {number} column - Column number
* @param {Record<string, any>} [additional] - Any additional properties to add to the dev stack entry
* @returns {any}
*/
export function add_svelte_meta(callback, type, component, line, column, additional) {
const parent = dev_stack;
dev_stack = {
type,
file: component[FILENAME],
line,
column,
parent,
...additional
};
try {
return callback();
} finally {
dev_stack = parent;
}
}
/**
* The current component function. Different from current component context:
* ```html

@ -2,6 +2,7 @@
import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants';
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js';
import { dev_stack } from '../context.js';
/**
* @param {any} fn
@ -28,6 +29,7 @@ export function add_locations(fn, filename, locations) {
function assign_location(element, filename, location) {
// @ts-expect-error
element.__svelte_meta = {
parent: dev_stack,
loc: { file: filename, line: location[0], column: location[1] }
};

@ -16,9 +16,11 @@ import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import {
component_context,
dev_stack,
is_runes,
set_component_context,
set_dev_current_component_function
set_dev_current_component_function,
set_dev_stack
} from '../../context.js';
const PENDING = 0;
@ -45,6 +47,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** @type {any} */
var component_function = DEV ? component_context?.function : null;
var dev_original_stack = DEV ? dev_stack : null;
/** @type {V | Promise<V> | typeof UNINITIALIZED} */
var input = UNINITIALIZED;
@ -75,7 +78,10 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
set_active_effect(effect);
set_active_reaction(effect); // TODO do we need both?
set_component_context(active_component_context);
if (DEV) set_dev_current_component_function(component_function);
if (DEV) {
set_dev_current_component_function(component_function);
set_dev_stack(dev_original_stack);
}
}
try {
@ -107,7 +113,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
}
} finally {
if (restore) {
if (DEV) set_dev_current_component_function(null);
if (DEV) {
set_dev_current_component_function(null);
set_dev_stack(null);
}
set_component_context(null);
set_active_reaction(null);
set_active_effect(null);

@ -1,6 +1,5 @@
/** @import { Effect, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants';
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import { component_context, set_component_context } from '../../context.js';
import { invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
@ -21,116 +20,170 @@ import {
import { queue_micro_task } from '../task.js';
/**
* @param {Effect} boundary
* @param {() => void} fn
* @typedef {{
* onerror?: (error: unknown, reset: () => void) => void;
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
* }} BoundaryProps
*/
function with_boundary(boundary, fn) {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_ctx = component_context;
set_active_effect(boundary);
set_active_reaction(boundary);
set_component_context(boundary.ctx);
try {
fn();
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_ctx);
}
}
var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
/**
* @param {TemplateNode} node
* @param {{
* onerror?: (error: unknown, reset: () => void) => void,
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
* }} props
* @param {((anchor: Node) => void)} boundary_fn
* @param {BoundaryProps} props
* @param {((anchor: Node) => void)} children
* @returns {void}
*/
export function boundary(node, props, boundary_fn) {
var anchor = node;
export function boundary(node, props, children) {
new Boundary(node, props, children);
}
export class Boundary {
/** @type {TemplateNode} */
#anchor;
/** @type {TemplateNode} */
#hydrate_open;
/** @type {BoundaryProps} */
#props;
/** @type {((anchor: Node) => void)} */
#children;
/** @type {Effect} */
var boundary_effect;
block(() => {
var boundary = /** @type {Effect} */ (active_effect);
var hydrate_open = hydrate_node;
var is_creating_fallback = false;
// We re-use the effect's fn property to avoid allocation of an additional field
boundary.fn = (/** @type {unknown}} */ error) => {
var onerror = props.onerror;
let failed = props.failed;
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if ((!onerror && !failed) || is_creating_fallback) {
throw error;
}
#effect;
var reset = () => {
pause_effect(boundary_effect);
/** @type {Effect | null} */
#main_effect = null;
with_boundary(boundary, () => {
is_creating_fallback = false;
boundary_effect = branch(() => boundary_fn(anchor));
});
};
/** @type {Effect | null} */
#failed_effect = null;
var previous_reaction = active_reaction;
#is_creating_fallback = false;
try {
set_active_reaction(null);
onerror?.(error, reset);
} finally {
set_active_reaction(previous_reaction);
/**
* @param {TemplateNode} node
* @param {BoundaryProps} props
* @param {((anchor: Node) => void)} children
*/
constructor(node, props, children) {
this.#anchor = node;
this.#props = props;
this.#children = children;
this.#hydrate_open = hydrate_node;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
if (hydrating) {
hydrate_next();
}
if (boundary_effect) {
destroy_effect(boundary_effect);
} else if (hydrating) {
set_hydrate_node(hydrate_open);
next();
set_hydrate_node(remove_nodes());
try {
this.#main_effect = branch(() => children(this.#anchor));
} catch (error) {
this.error(error);
}
}, flags);
if (failed) {
// Render the `failed` snippet in a microtask
queue_micro_task(() => {
with_boundary(boundary, () => {
is_creating_fallback = true;
try {
boundary_effect = branch(() => {
failed(
anchor,
() => error,
() => reset
);
});
} catch (error) {
invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent));
}
is_creating_fallback = false;
});
if (hydrating) {
this.#anchor = hydrate_node;
}
}
/**
* @param {() => Effect | null} fn
*/
#run(fn) {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_ctx = component_context;
set_active_effect(this.#effect);
set_active_reaction(this.#effect);
set_component_context(this.#effect.ctx);
try {
return fn();
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_ctx);
}
}
/** @param {unknown} error */
error(error) {
var onerror = this.#props.onerror;
let failed = this.#props.failed;
const reset = () => {
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
this.#failed_effect = null;
});
}
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
return branch(() => this.#children(this.#anchor));
});
};
if (hydrating) {
hydrate_next();
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if (this.#is_creating_fallback || (!onerror && !failed)) {
throw error;
}
var previous_reaction = active_reaction;
try {
set_active_reaction(null);
onerror?.(error, reset);
} finally {
set_active_reaction(previous_reaction);
}
boundary_effect = branch(() => boundary_fn(anchor));
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT);
if (this.#main_effect) {
destroy_effect(this.#main_effect);
this.#main_effect = null;
}
if (hydrating) {
anchor = hydrate_node;
if (this.#failed_effect) {
destroy_effect(this.#failed_effect);
this.#failed_effect = null;
}
if (hydrating) {
set_hydrate_node(this.#hydrate_open);
next();
set_hydrate_node(remove_nodes());
}
if (failed) {
queue_micro_task(() => {
this.#failed_effect = this.#run(() => {
this.#is_creating_fallback = true;
try {
return branch(() => {
failed(
this.#anchor,
() => error,
() => reset
);
});
} catch (error) {
invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent));
return null;
} finally {
this.#is_creating_fallback = false;
}
});
});
}
}
}

@ -18,7 +18,7 @@ import {
import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
import { active_effect } from '../../runtime.js';
import { component_context } from '../../context.js';
import { component_context, dev_stack } from '../../context.js';
import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { assign_nodes } from '../template.js';
@ -107,6 +107,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
if (DEV && location) {
// @ts-expect-error
element.__svelte_meta = {
parent: dev_stack,
loc: {
file: filename,
line: location[0],

@ -20,7 +20,7 @@ import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
import { set_style } from './style.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js';
import { block, branch, destroy_effect } from '../../reactivity/effects.js';
import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js';
import { derived } from '../../reactivity/deriveds.js';
import { init_select, select_option } from './bindings/select.js';
@ -513,10 +513,13 @@ export function attribute_effect(
});
if (is_select) {
init_select(
/** @type {HTMLSelectElement} */ (element),
() => /** @type {Record<string | symbol, any>} */ (prev).value
);
var select = /** @type {HTMLSelectElement} */ (element);
var mounting = true;
effect(() => {
select_option(select, /** @type {Record<string | symbol, any>} */ (prev).value, mounting);
mounting = false;
init_select(select);
});
}
inited = true;

@ -1,6 +1,5 @@
import { effect } from '../../../reactivity/effects.js';
import { effect, teardown } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js';
import { untrack } from '../../../runtime.js';
import { is } from '../../../proxy.js';
import { is_array } from '../../../../shared/utils.js';
import * as w from '../../../warnings.js';
@ -51,40 +50,29 @@ export function select_option(select, value, mounting) {
* current selection to the dom when it changes. Such
* changes could for example occur when options are
* inside an `#each` block.
* @template V
* @param {HTMLSelectElement} select
* @param {() => V} [get_value]
*/
export function init_select(select, get_value) {
let mounting = true;
effect(() => {
if (get_value) {
select_option(select, untrack(get_value), mounting);
}
mounting = false;
export function init_select(select) {
var observer = new MutationObserver(() => {
// @ts-ignore
select_option(select, select.__value);
// Deliberately don't update the potential binding value,
// the model should be preserved unless explicitly changed
});
observer.observe(select, {
// Listen to option element changes
childList: true,
subtree: true, // because of <optgroup>
// Listen to option element value attribute changes
// (doesn't get notified of select value changes,
// because that property is not reflected as an attribute)
attributes: true,
attributeFilter: ['value']
});
var observer = new MutationObserver(() => {
// @ts-ignore
var value = select.__value;
select_option(select, value);
// Deliberately don't update the potential binding value,
// the model should be preserved unless explicitly changed
});
observer.observe(select, {
// Listen to option element changes
childList: true,
subtree: true, // because of <optgroup>
// Listen to option element value attribute changes
// (doesn't get notified of select value changes,
// because that property is not reflected as an attribute)
attributes: true,
attributeFilter: ['value']
});
return () => {
observer.disconnect();
};
teardown(() => {
observer.disconnect();
});
}
@ -136,7 +124,6 @@ export function bind_select_value(select, get, set = get) {
mounting = false;
});
// don't pass get_value, we already initialize it in the effect above
init_select(select);
}

@ -1,4 +1,5 @@
/** @import { Effect } from '#client' */
/** @import { Boundary } from './dom/blocks/boundary.js' */
import { DEV } from 'esm-env';
import { FILENAME } from '../../constants.js';
import { is_firefox } from './dom/operations.js';
@ -39,8 +40,7 @@ export function invoke_error_boundary(error, effect) {
while (effect !== null) {
if ((effect.f & BOUNDARY_EFFECT) !== 0) {
try {
// @ts-expect-error
effect.fn(error);
/** @type {Boundary} */ (effect.b).error(error);
return;
} catch {}
}

@ -11,6 +11,7 @@ export function bind_invalid_checkbox_value() {
const error = new Error(`bind_invalid_checkbox_value\nUsing \`bind:value\` together with a checkbox input is not allowed. Use \`bind:checked\` instead\nhttps://svelte.dev/e/bind_invalid_checkbox_value`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_invalid_checkbox_value`);
@ -29,6 +30,7 @@ export function bind_invalid_export(component, key, name) {
const error = new Error(`bind_invalid_export\nComponent ${component} has an export named \`${key}\` that a consumer component is trying to access using \`bind:${key}\`, which is disallowed. Instead, use \`bind:this\` (e.g. \`<${name} bind:this={component} />\`) and then access the property on the bound component instance (e.g. \`component.${key}\`)\nhttps://svelte.dev/e/bind_invalid_export`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_invalid_export`);
@ -47,6 +49,7 @@ export function bind_not_bindable(key, component, name) {
const error = new Error(`bind_not_bindable\nA component is attempting to bind to a non-bindable property \`${key}\` belonging to ${component} (i.e. \`<${name} bind:${key}={...}>\`). To mark a property as bindable: \`let { ${key} = $bindable() } = $props()\`\nhttps://svelte.dev/e/bind_not_bindable`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_not_bindable`);
@ -64,6 +67,7 @@ export function component_api_changed(method, component) {
const error = new Error(`component_api_changed\nCalling \`${method}\` on a component instance (of ${component}) is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/component_api_changed`);
@ -81,6 +85,7 @@ export function component_api_invalid_new(component, name) {
const error = new Error(`component_api_invalid_new\nAttempted to instantiate ${component} with \`new ${name}\`, which is no longer valid in Svelte 5. If this component is not under your control, set the \`compatibility.componentApi\` compiler option to \`4\` to keep it working.\nhttps://svelte.dev/e/component_api_invalid_new`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/component_api_invalid_new`);
@ -96,6 +101,7 @@ export function derived_references_self() {
const error = new Error(`derived_references_self\nA derived value cannot reference itself recursively\nhttps://svelte.dev/e/derived_references_self`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/derived_references_self`);
@ -111,9 +117,12 @@ export function derived_references_self() {
*/
export function each_key_duplicate(a, b, value) {
if (DEV) {
const error = new Error(`each_key_duplicate\n${value ? `Keyed each block has duplicate key \`${value}\` at indexes ${a} and ${b}` : `Keyed each block has duplicate key at indexes ${a} and ${b}`}\nhttps://svelte.dev/e/each_key_duplicate`);
const error = new Error(`each_key_duplicate\n${value
? `Keyed each block has duplicate key \`${value}\` at indexes ${a} and ${b}`
: `Keyed each block has duplicate key at indexes ${a} and ${b}`}\nhttps://svelte.dev/e/each_key_duplicate`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/each_key_duplicate`);
@ -130,6 +139,7 @@ export function effect_in_teardown(rune) {
const error = new Error(`effect_in_teardown\n\`${rune}\` cannot be used inside an effect cleanup function\nhttps://svelte.dev/e/effect_in_teardown`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_in_teardown`);
@ -145,6 +155,7 @@ export function effect_in_unowned_derived() {
const error = new Error(`effect_in_unowned_derived\nEffect cannot be created inside a \`$derived\` value that was not itself created inside an effect\nhttps://svelte.dev/e/effect_in_unowned_derived`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_in_unowned_derived`);
@ -161,6 +172,7 @@ export function effect_orphan(rune) {
const error = new Error(`effect_orphan\n\`${rune}\` can only be used inside an effect (e.g. during component initialisation)\nhttps://svelte.dev/e/effect_orphan`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_orphan`);
@ -176,12 +188,29 @@ export function effect_update_depth_exceeded() {
const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops\nhttps://svelte.dev/e/effect_update_depth_exceeded`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_update_depth_exceeded`);
}
}
/**
* `getAbortSignal()` can only be called inside an effect or derived
* @returns {never}
*/
export function get_abort_signal_outside_reaction() {
if (DEV) {
const error = new Error(`get_abort_signal_outside_reaction\n\`getAbortSignal()\` can only be called inside an effect or derived\nhttps://svelte.dev/e/get_abort_signal_outside_reaction`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`);
}
}
/**
* Failed to hydrate the application
* @returns {never}
@ -191,6 +220,7 @@ export function hydration_failed() {
const error = new Error(`hydration_failed\nFailed to hydrate the application\nhttps://svelte.dev/e/hydration_failed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/hydration_failed`);
@ -206,6 +236,7 @@ export function invalid_snippet() {
const error = new Error(`invalid_snippet\nCould not \`{@render}\` snippet due to the expression being \`null\` or \`undefined\`. Consider using optional chaining \`{@render snippet?.()}\`\nhttps://svelte.dev/e/invalid_snippet`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_snippet`);
@ -222,6 +253,7 @@ export function lifecycle_legacy_only(name) {
const error = new Error(`lifecycle_legacy_only\n\`${name}(...)\` cannot be used in runes mode\nhttps://svelte.dev/e/lifecycle_legacy_only`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/lifecycle_legacy_only`);
@ -238,6 +270,7 @@ export function props_invalid_value(key) {
const error = new Error(`props_invalid_value\nCannot do \`bind:${key}={undefined}\` when \`${key}\` has a fallback value\nhttps://svelte.dev/e/props_invalid_value`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/props_invalid_value`);
@ -254,6 +287,7 @@ export function props_rest_readonly(property) {
const error = new Error(`props_rest_readonly\nRest element properties of \`$props()\` such as \`${property}\` are readonly\nhttps://svelte.dev/e/props_rest_readonly`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/props_rest_readonly`);
@ -270,6 +304,7 @@ export function rune_outside_svelte(rune) {
const error = new Error(`rune_outside_svelte\nThe \`${rune}\` rune is only available inside \`.svelte\` and \`.svelte.js/ts\` files\nhttps://svelte.dev/e/rune_outside_svelte`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/rune_outside_svelte`);
@ -285,6 +320,7 @@ export function state_descriptors_fixed() {
const error = new Error(`state_descriptors_fixed\nProperty descriptors defined on \`$state\` objects must contain \`value\` and always be \`enumerable\`, \`configurable\` and \`writable\`.\nhttps://svelte.dev/e/state_descriptors_fixed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_descriptors_fixed`);
@ -300,6 +336,7 @@ export function state_prototype_fixed() {
const error = new Error(`state_prototype_fixed\nCannot set prototype of \`$state\` object\nhttps://svelte.dev/e/state_prototype_fixed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_prototype_fixed`);
@ -315,6 +352,7 @@ export function state_unsafe_mutation() {
const error = new Error(`state_unsafe_mutation\nUpdating state inside \`$derived(...)\`, \`$inspect(...)\` or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_unsafe_mutation`);

@ -1,6 +1,6 @@
export { createAttachmentKey as attachment } from '../../attachments/index.js';
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { push, pop } from './context.js';
export { push, pop, add_svelte_meta } from './context.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';

@ -1,6 +1,6 @@
/** @import { Derived, Effect } from '#client' */
import { DEV } from 'esm-env';
import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '#client/constants';
import { CLEAN, DERIVED, DIRTY, EFFECT_PRESERVED, MAYBE_DIRTY, UNOWNED } from '#client/constants';
import {
active_reaction,
active_effect,
@ -38,7 +38,7 @@ export function derived(fn) {
} else {
// Since deriveds are evaluated lazily, any effects created inside them are
// created too late to ensure that the parent effect is added to the tree
active_effect.f |= EFFECT_HAS_DERIVED;
active_effect.f |= EFFECT_PRESERVED;
}
/** @type {Derived<V>} */
@ -53,7 +53,8 @@ export function derived(fn) {
rv: 0,
v: /** @type {V} */ (null),
wv: 0,
parent: parent_derived ?? active_effect
parent: parent_derived ?? active_effect,
ac: null
};
if (DEV && tracing_mode_flag) {

@ -31,8 +31,9 @@ import {
INSPECT_EFFECT,
HEAD_EFFECT,
MAYBE_DIRTY,
EFFECT_HAS_DERIVED,
BOUNDARY_EFFECT
EFFECT_PRESERVED,
BOUNDARY_EFFECT,
STALE_REACTION
} from '#client/constants';
import { set } from './sources.js';
import * as e from '../errors.js';
@ -40,7 +41,7 @@ import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { derived } from './deriveds.js';
import { component_context, dev_current_component_function } from '../context.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -103,10 +104,12 @@ function create_effect(type, fn, sync, push = true) {
last: null,
next: null,
parent,
b: parent && parent.b,
prev: null,
teardown: null,
transitions: null,
wv: 0
wv: 0,
ac: null
};
if (DEV) {
@ -133,7 +136,7 @@ function create_effect(type, fn, sync, push = true) {
effect.first === null &&
effect.nodes_start === null &&
effect.teardown === null &&
(effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0;
(effect.f & (EFFECT_PRESERVED | BOUNDARY_EFFECT)) === 0;
if (!inert && push) {
if (parent !== null) {
@ -357,7 +360,11 @@ export function template_effect(fn, thunks = [], d = derived) {
* @param {number} flags
*/
export function block(fn, flags = 0) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
var effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
if (DEV) {
effect.dev_stack = dev_stack;
}
return effect;
}
/**
@ -397,6 +404,8 @@ export function destroy_effect_children(signal, remove_dom = false) {
signal.first = signal.last = null;
while (effect !== null) {
effect.ac?.abort(STALE_REACTION);
var next = effect.next;
if ((effect.f & ROOT_EFFECT) !== 0) {
@ -478,6 +487,7 @@ export function destroy_effect(effect, remove_dom = true) {
effect.fn =
effect.nodes_start =
effect.nodes_end =
effect.ac =
null;
}

@ -8,12 +8,11 @@ import {
PROPS_IS_UPDATED
} from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js';
import { mutable_source, set, source, update } from './sources.js';
import { set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
import { get, captured_signals, untrack } from '../runtime.js';
import { safe_equals } from './equality.js';
import { get, untrack } from '../runtime.js';
import * as e from '../errors.js';
import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
@ -260,89 +259,92 @@ function has_destroyed_component_ctx(current_value) {
* @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))}
*/
export function prop(props, key, flags, fallback) {
var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0;
var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0;
var bindable = (flags & PROPS_IS_BINDABLE) !== 0;
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
var is_store_sub = false;
var prop_value;
if (bindable) {
[prop_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
} else {
prop_value = /** @type {V} */ (props[key]);
}
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)`
var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
var setter =
(bindable &&
(get_descriptor(props, key)?.set ??
(is_entry_props && key in props && ((v) => (props[key] = v))))) ||
undefined;
var fallback_value = /** @type {V} */ (fallback);
var fallback_dirty = true;
var fallback_used = false;
var get_fallback = () => {
fallback_used = true;
if (fallback_dirty) {
fallback_dirty = false;
if (lazy) {
fallback_value = untrack(/** @type {() => V} */ (fallback));
} else {
fallback_value = /** @type {V} */ (fallback);
}
fallback_value = lazy
? untrack(/** @type {() => V} */ (fallback))
: /** @type {V} */ (fallback);
}
return fallback_value;
};
if (prop_value === undefined && fallback !== undefined) {
if (setter && runes) {
e.props_invalid_value(key);
}
/** @type {((v: V) => void) | undefined} */
var setter;
prop_value = get_fallback();
if (setter) setter(prop_value);
if (bindable) {
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)`
var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
setter =
get_descriptor(props, key)?.set ??
(is_entry_props && key in props ? (v) => (props[key] = v) : undefined);
}
var initial_value;
var is_store_sub = false;
if (bindable) {
[initial_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
} else {
initial_value = /** @type {V} */ (props[key]);
}
if (initial_value === undefined && fallback !== undefined) {
initial_value = get_fallback();
if (setter) {
if (runes) e.props_invalid_value(key);
setter(initial_value);
}
}
/** @type {() => V} */
var getter;
if (runes) {
getter = () => {
var value = /** @type {V} */ (props[key]);
if (value === undefined) return get_fallback();
fallback_dirty = true;
fallback_used = false;
return value;
};
} else {
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
// Replicate that behavior through using a derived
var derived_getter = (immutable ? derived : derived_safe_equal)(
() => /** @type {V} */ (props[key])
);
derived_getter.f |= LEGACY_DERIVED_PROP;
getter = () => {
var value = get(derived_getter);
if (value !== undefined) fallback_value = /** @type {V} */ (undefined);
var value = /** @type {V} */ (props[key]);
if (value !== undefined) {
// in legacy mode, we don't revert to the fallback value
// if the prop goes from defined to undefined. The easiest
// way to model this is to make the fallback undefined
// as soon as the prop has a value
fallback_value = /** @type {V} */ (undefined);
}
return value === undefined ? fallback_value : value;
};
}
// easy mode — prop is never written to
if ((flags & PROPS_IS_UPDATED) === 0 && runes) {
// prop is never written to — we only need a getter
if (runes && (flags & PROPS_IS_UPDATED) === 0) {
return getter;
}
// intermediate mode — prop is written to, but the parent component had
// `bind:foo` which means we can just call `$$props.foo = value` directly
// prop is written to, but the parent component had `bind:foo` which
// means we can just call `$$props.foo = value` directly
if (setter) {
var legacy_parent = props.$$legacy;
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode.
@ -352,82 +354,41 @@ export function prop(props, key, flags, fallback) {
if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value);
}
return value;
} else {
return getter();
}
return getter();
};
}
// hard mode. this is where it gets ugly — the value in the child should
// synchronize with the parent, but it should also be possible to temporarily
// set the value to something else locally.
var from_child = false;
var was_from_child = false;
// The derived returns the current value. The underlying mutable
// source is written to from various places to persist this value.
var inner_current_value = mutable_source(prop_value);
var current_value = derived(() => {
var parent_value = getter();
var child_value = get(inner_current_value);
if (from_child) {
from_child = false;
was_from_child = true;
return child_value;
}
was_from_child = false;
return (inner_current_value.v = parent_value);
});
// Ensure we eagerly capture the initial value if it's bindable
if (bindable) {
get(current_value);
}
// Either prop is written to, but there's no binding, which means we
// create a derived that we can write to locally.
// Or we are in legacy mode where we always create a derived to replicate that
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter);
if (!immutable) current_value.equals = safe_equals;
// Capture the initial value if it's bindable
if (bindable) get(d);
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
// legacy nonsense — need to ensure the source is invalidated when necessary
// also needed for when handling inspect logic so we can inspect the correct source signal
if (captured_signals !== null) {
// set this so that we don't reset to the parent value if `d`
// is invalidated because of `invalidate_inner_signals` (rather
// than because the parent or child value changed)
from_child = was_from_child;
// invoke getters so that signals are picked up by `invalidate_inner_signals`
getter();
get(inner_current_value);
}
if (arguments.length > 0) {
const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value;
if (!current_value.equals(new_value)) {
from_child = true;
set(inner_current_value, new_value);
// To ensure the fallback value is consistent when used with proxies, we
// update the local fallback_value, but only if the fallback is actively used
if (fallback_used && fallback_value !== undefined) {
fallback_value = new_value;
}
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
if (has_destroyed_component_ctx(current_value)) {
return value;
}
set(d, new_value);
untrack(() => get(current_value)); // force a synchronisation immediately
if (fallback_value !== undefined) {
fallback_value = new_value;
}
return value;
}
if (has_destroyed_component_ctx(current_value)) {
return current_value.v;
// TODO is this still necessary post-#16263?
if (has_destroyed_component_ctx(d)) {
return d.v;
}
return get(current_value);
return get(d);
};
}

@ -11,7 +11,7 @@ import {
untrack,
increment_write_version,
update_effect,
reaction_sources,
source_ownership,
check_dirtiness,
untracking,
is_destroying_effect,
@ -140,7 +140,7 @@ export function set(source, value, should_proxy = false) {
(!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) &&
is_runes() &&
(active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 &&
!(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction)
!(source_ownership?.reaction === active_reaction && source_ownership.sources.includes(source))
) {
e.state_unsafe_mutation();
}

@ -1,4 +1,11 @@
import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client';
import type {
ComponentContext,
DevStackEntry,
Equals,
TemplateNode,
TransitionManager
} from '#client';
import type { Boundary } from '../dom/blocks/boundary';
export interface Signal {
/** Flags bitmask */
@ -40,6 +47,8 @@ export interface Reaction extends Signal {
fn: null | Function;
/** Signals that this signal reads from */
deps: null | Value[];
/** An AbortController that aborts when the signal is destroyed */
ac: null | AbortController;
}
export interface Derived<V = unknown> extends Value<V>, Reaction {
@ -76,8 +85,12 @@ export interface Effect extends Reaction {
last: null | Effect;
/** Parent effect */
parent: Effect | null;
/** The boundary this effect belongs to */
b: Boundary | null;
/** Dev only */
component_function?: any;
/** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */
dev_stack?: DevStackEntry | null;
}
export type Source<V = unknown> = Value<V>;

@ -20,9 +20,9 @@ import {
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT,
LEGACY_DERIVED_PROP,
DISCONNECTED,
EFFECT_IS_UPDATING
EFFECT_IS_UPDATING,
STALE_REACTION
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { internal_set, old_values } from './reactivity/sources.js';
@ -34,12 +34,13 @@ import { tracing_expressions, get_stack } from './dev/tracing.js';
import {
component_context,
dev_current_component_function,
dev_stack,
is_runes,
set_component_context,
set_dev_current_component_function
set_dev_current_component_function,
set_dev_stack
} from './context.js';
import { handle_error, invoke_error_boundary } from './error-handling.js';
import { snapshot } from '../shared/clone.js';
let is_flushing = false;
@ -85,17 +86,17 @@ export function set_active_effect(effect) {
/**
* When sources are created within a reaction, reading and writing
* them within that reaction should not cause a re-run
* @type {null | [active_reaction: Reaction, sources: Source[]]}
* @type {null | { reaction: Reaction, sources: Source[] }}
*/
export let reaction_sources = null;
export let source_ownership = null;
/** @param {Value} value */
export function push_reaction_value(value) {
if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
if (reaction_sources === null) {
reaction_sources = [active_reaction, [value]];
if (source_ownership === null) {
source_ownership = { reaction: active_reaction, sources: [value] };
} else {
reaction_sources[1].push(value);
source_ownership.sources.push(value);
}
}
}
@ -234,7 +235,12 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
for (var i = 0; i < reactions.length; i++) {
var reaction = reactions[i];
if (reaction_sources?.[1].includes(signal) && reaction_sources[0] === active_reaction) continue;
if (
source_ownership?.reaction === active_reaction &&
source_ownership.sources.includes(signal)
) {
continue;
}
if ((reaction.f & DERIVED) !== 0) {
schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
@ -256,7 +262,7 @@ export function update_reaction(reaction) {
var previous_untracked_writes = untracked_writes;
var previous_reaction = active_reaction;
var previous_skip_reaction = skip_reaction;
var previous_reaction_sources = reaction_sources;
var previous_reaction_sources = source_ownership;
var previous_component_context = component_context;
var previous_untracking = untracking;
@ -269,13 +275,18 @@ export function update_reaction(reaction) {
(flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
reaction_sources = null;
source_ownership = null;
set_component_context(reaction.ctx);
untracking = false;
read_version++;
reaction.f |= EFFECT_IS_UPDATING;
if (reaction.ac !== null) {
reaction.ac.abort(STALE_REACTION);
reaction.ac = null;
}
try {
var result = /** @type {Function} */ (0, reaction.fn)();
var deps = reaction.deps;
@ -294,7 +305,12 @@ export function update_reaction(reaction) {
reaction.deps = deps = new_deps;
}
if (!skip_reaction) {
if (
!skip_reaction ||
// Deriveds that already have reactions can cleanup, so we still add them as reactions
((flags & DERIVED) !== 0 &&
/** @type {import('#client').Derived} */ (reaction).reactions !== null)
) {
for (i = skipped_deps; i < deps.length; i++) {
(deps[i].reactions ??= []).push(reaction);
}
@ -347,7 +363,7 @@ export function update_reaction(reaction) {
untracked_writes = previous_untracked_writes;
active_reaction = previous_reaction;
skip_reaction = previous_skip_reaction;
reaction_sources = previous_reaction_sources;
source_ownership = previous_reaction_sources;
set_component_context(previous_component_context);
untracking = previous_untracking;
@ -434,6 +450,9 @@ export function update_effect(effect) {
if (DEV) {
var previous_component_fn = dev_current_component_function;
set_dev_current_component_function(effect.component_function);
var previous_stack = /** @type {any} */ (dev_stack);
// only block effects have a dev stack, keep the current one otherwise
set_dev_stack(effect.dev_stack ?? dev_stack);
}
try {
@ -468,6 +487,7 @@ export function update_effect(effect) {
if (DEV) {
set_dev_current_component_function(previous_component_fn);
set_dev_stack(previous_stack);
}
}
}
@ -724,7 +744,10 @@ export function get(signal) {
// Register the dependency on the current reaction signal.
if (active_reaction !== null && !untracking) {
if (!reaction_sources?.[1].includes(signal) || reaction_sources[0] !== active_reaction) {
if (
source_ownership?.reaction !== active_reaction ||
!source_ownership?.sources.includes(signal)
) {
var deps = active_reaction.deps;
if (signal.rv < read_version) {
signal.rv = read_version;
@ -852,17 +875,7 @@ export function invalidate_inner_signals(fn) {
var captured = capture_signals(() => untrack(fn));
for (var signal of captured) {
// Go one level up because derived signals created as part of props in legacy mode
if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
for (const dep of /** @type {Derived} */ (signal).deps || []) {
if ((dep.f & DERIVED) === 0) {
// Use internal_set instead of set here and below to avoid mutation validation
internal_set(dep, dep.v);
}
}
} else {
internal_set(signal, signal.v);
}
internal_set(signal, signal.v);
}
}

@ -187,4 +187,13 @@ export type SourceLocation =
| [line: number, column: number]
| [line: number, column: number, SourceLocation[]];
export interface DevStackEntry {
file: string;
type: 'component' | 'if' | 'each' | 'await' | 'key' | 'render';
line: number;
column: number;
parent: DevStackEntry | null;
componentTag?: string;
}
export * from './reactivity/types';

@ -25,7 +25,13 @@ export function assignment_value_stale(property, location) {
*/
export function binding_property_non_reactive(binding, location) {
if (DEV) {
console.warn(`%c[svelte] binding_property_non_reactive\n%c${location ? `\`${binding}\` (${location}) is binding to a non-reactive property` : `\`${binding}\` is binding to a non-reactive property`}\nhttps://svelte.dev/e/binding_property_non_reactive`, bold, normal);
console.warn(
`%c[svelte] binding_property_non_reactive\n%c${location
? `\`${binding}\` (${location}) is binding to a non-reactive property`
: `\`${binding}\` is binding to a non-reactive property`}\nhttps://svelte.dev/e/binding_property_non_reactive`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/binding_property_non_reactive`);
}
@ -76,7 +82,13 @@ export function hydration_attribute_changed(attribute, html, value) {
*/
export function hydration_html_changed(location) {
if (DEV) {
console.warn(`%c[svelte] hydration_html_changed\n%c${location ? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value` : 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'}\nhttps://svelte.dev/e/hydration_html_changed`, bold, normal);
console.warn(
`%c[svelte] hydration_html_changed\n%c${location
? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value`
: 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'}\nhttps://svelte.dev/e/hydration_html_changed`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/hydration_html_changed`);
}
@ -88,7 +100,13 @@ export function hydration_html_changed(location) {
*/
export function hydration_mismatch(location) {
if (DEV) {
console.warn(`%c[svelte] hydration_mismatch\n%c${location ? `Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near ${location}` : 'Hydration failed because the initial UI does not match what was rendered on the server'}\nhttps://svelte.dev/e/hydration_mismatch`, bold, normal);
console.warn(
`%c[svelte] hydration_mismatch\n%c${location
? `Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near ${location}`
: 'Hydration failed because the initial UI does not match what was rendered on the server'}\nhttps://svelte.dev/e/hydration_mismatch`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/hydration_mismatch`);
}

@ -0,0 +1,13 @@
import { STALE_REACTION } from '#client/constants';
/** @type {AbortController | null} */
export let controller = null;
export function abort() {
controller?.abort(STALE_REACTION);
controller = null;
}
export function getAbortSignal() {
return (controller ??= new AbortController()).signal;
}

@ -1,5 +1,7 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
/**
* `%name%(...)` is not available on the server
* @param {string} name
@ -9,5 +11,6 @@ export function lifecycle_function_unavailable(name) {
const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server\nhttps://svelte.dev/e/lifecycle_function_unavailable`);
error.name = 'Svelte error';
throw error;
}

@ -18,6 +18,7 @@ import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import { reset_elements } from './dev.js';
import { Payload } from './payload.js';
import { abort } from './abort-signal.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter
@ -66,50 +67,54 @@ export let on_destroy = [];
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
try {
const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
const prev_on_destroy = on_destroy;
on_destroy = [];
payload.out += BLOCK_OPEN;
const prev_on_destroy = on_destroy;
on_destroy = [];
payload.out += BLOCK_OPEN;
let reset_reset_element;
let reset_reset_element;
if (DEV) {
// prevent parent/child element state being corrupted by a bad render
reset_reset_element = reset_elements();
}
if (DEV) {
// prevent parent/child element state being corrupted by a bad render
reset_reset_element = reset_elements();
}
if (options.context) {
push();
/** @type {Component} */ (current_component).c = options.context;
}
if (options.context) {
push();
/** @type {Component} */ (current_component).c = options.context;
}
// @ts-expect-error
component(payload, options.props ?? {}, {}, {});
// @ts-expect-error
component(payload, options.props ?? {}, {}, {});
if (options.context) {
pop();
}
if (options.context) {
pop();
}
if (reset_reset_element) {
reset_reset_element();
}
if (reset_reset_element) {
reset_reset_element();
}
payload.out += BLOCK_CLOSE;
for (const cleanup of on_destroy) cleanup();
on_destroy = prev_on_destroy;
payload.out += BLOCK_CLOSE;
for (const cleanup of on_destroy) cleanup();
on_destroy = prev_on_destroy;
let head = payload.head.out + payload.head.title;
let head = payload.head.out + payload.head.title;
for (const { hash, code } of payload.css) {
head += `<style id="${hash}">${code}</style>`;
}
for (const { hash, code } of payload.css) {
head += `<style id="${hash}">${code}</style>`;
}
return {
head,
html: payload.out,
body: payload.out
};
return {
head,
html: payload.out,
body: payload.out
};
} finally {
abort();
}
}
/**

@ -11,6 +11,7 @@ export function invalid_default_snippet() {
const error = new Error(`invalid_default_snippet\nCannot use \`{@render children(...)}\` if the parent component uses \`let:\` directives. Consider using a named snippet instead\nhttps://svelte.dev/e/invalid_default_snippet`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_default_snippet`);
@ -26,6 +27,7 @@ export function invalid_snippet_arguments() {
const error = new Error(`invalid_snippet_arguments\nA snippet function was passed invalid arguments. Snippets should only be instantiated via \`{@render ...}\`\nhttps://svelte.dev/e/invalid_snippet_arguments`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`);
@ -42,6 +44,7 @@ export function lifecycle_outside_component(name) {
const error = new Error(`lifecycle_outside_component\n\`${name}(...)\` can only be used during component initialisation\nhttps://svelte.dev/e/lifecycle_outside_component`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/lifecycle_outside_component`);
@ -57,6 +60,7 @@ export function snippet_without_render_tag() {
const error = new Error(`snippet_without_render_tag\nAttempted to render a snippet without a \`{@render}\` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change \`{snippet}\` to \`{@render snippet()}\`.\nhttps://svelte.dev/e/snippet_without_render_tag`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/snippet_without_render_tag`);
@ -73,6 +77,7 @@ export function store_invalid_shape(name) {
const error = new Error(`store_invalid_shape\n\`${name}\` is not a store with a \`subscribe\` method\nhttps://svelte.dev/e/store_invalid_shape`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/store_invalid_shape`);
@ -88,6 +93,7 @@ export function svelte_element_invalid_this_value() {
const error = new Error(`svelte_element_invalid_this_value\nThe \`this\` prop on \`<svelte:element>\` must be a string, if defined\nhttps://svelte.dev/e/svelte_element_invalid_this_value`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/svelte_element_invalid_this_value`);

@ -25,11 +25,15 @@ export function dynamic_void_element_content(tag) {
*/
export function state_snapshot_uncloneable(properties) {
if (DEV) {
console.warn(`%c[svelte] state_snapshot_uncloneable\n%c${properties
? `The following properties cannot be cloned with \`$state.snapshot\` — the return value contains the originals:
console.warn(
`%c[svelte] state_snapshot_uncloneable\n%c${properties
? `The following properties cannot be cloned with \`$state.snapshot\` — the return value contains the originals:
${properties}`
: 'Value cannot be cloned with `$state.snapshot` — the original value was returned'}\nhttps://svelte.dev/e/state_snapshot_uncloneable`, bold, normal);
: 'Value cannot be cloned with `$state.snapshot` — the original value was returned'}\nhttps://svelte.dev/e/state_snapshot_uncloneable`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/state_snapshot_uncloneable`);
}

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.34.8';
export const VERSION = '5.35.2';
export const PUBLIC_VERSION = '5';

@ -21,6 +21,8 @@ const { test, run } = suite<ParserTest>(async (config, cwd) => {
)
);
delete actual.comments;
// run `UPDATE_SNAPSHOTS=true pnpm test parser` to update parser tests
if (process.env.UPDATE_SNAPSHOTS) {
fs.writeFileSync(`${cwd}/output.json`, JSON.stringify(actual, null, '\t'));

@ -2,6 +2,7 @@ import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
accessors: false,
test({ assert, logs, target }) {
assert.deepEqual(logs, ['primitive', 'object']);
target.querySelector('button')?.click();

@ -0,0 +1,34 @@
import { test } from '../../test';
export default test({
html: `<button>increment</button><p>loading...</p>`,
async test({ assert, target, variant, logs }) {
await new Promise((f) => setTimeout(f, 50));
if (variant === 'hydrate') {
assert.deepEqual(logs, [
'aborted',
'StaleReactionError',
'The reaction that called `getAbortSignal()` was re-run or destroyed'
]);
}
logs.length = 0;
const [button] = target.querySelectorAll('button');
await new Promise((f) => setTimeout(f, 50));
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>0</p>');
button.click();
await new Promise((f) => setTimeout(f, 50));
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>2</p>');
assert.deepEqual(logs, [
'aborted',
'StaleReactionError',
'The reaction that called `getAbortSignal()` was re-run or destroyed'
]);
}
});

@ -0,0 +1,33 @@
<script>
import { getAbortSignal } from 'svelte';
let count = $state(0);
let delayed_count = $derived.by(async () => {
let c = count;
const signal = getAbortSignal();
await new Promise((f) => setTimeout(f));
if (signal.aborted) {
console.log('aborted', signal.reason.name, signal.reason.message);
}
return c;
});
</script>
<button onclick={async () => {
count += 1;
await Promise.resolve();
count += 1;
}}>increment</button>
{#await delayed_count}
<p>loading...</p>
{:then count}
<p>{count}</p>
{:catch error}
{console.log('this should never be rendered')}
{/await}

@ -5,5 +5,9 @@ export default test({
compileOptions: {
dev: true
},
error: 'state_unsafe_mutation'
error: 'state_unsafe_mutation',
// silence the logs
test({ logs }) {}
});

@ -6,7 +6,7 @@ export default test({
compileOptions: {
runes: undefined
},
async test({ assert, target }) {
async test({ assert, target, logs }) {
const p = target.querySelector('p');
const btn = target.querySelector('button');
flushSync(() => {

@ -0,0 +1,9 @@
import { ok, test } from '../../test';
export default test({
async test({ assert, target, instance }) {
const select = target.querySelector('select');
ok(select);
assert.equal(select.selectedIndex, 1);
}
});

@ -0,0 +1,8 @@
<script>
let others = {onclick: ()=> {}}
</script>
<select {...others}>
<option>o1</option>
<option selected>o2</option>
</select>

@ -0,0 +1,186 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
compileOptions: {
dev: true
},
html: `
<p>no parent</p>
<button>toggle</button>
<p>if</p>
<p>each</p>
<p>loading</p>
<p>key</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
`,
async test({ target, assert }) {
function check() {
const [main, if_, each, await_, key, child1, child2, child3, child4, dynamic] =
target.querySelectorAll('p');
// @ts-expect-error
assert.deepEqual(main.__svelte_meta.parent, null);
// @ts-expect-error
assert.deepEqual(if_.__svelte_meta.parent, {
file: 'main.svelte',
type: 'if',
line: 12,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(each.__svelte_meta.parent, {
file: 'main.svelte',
type: 'each',
line: 16,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(await_.__svelte_meta.parent, {
file: 'main.svelte',
type: 'await',
line: 20,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(key.__svelte_meta.parent, {
file: 'main.svelte',
type: 'key',
line: 26,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(child1.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 30,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(child2.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 33,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 32,
column: 0,
parent: null
}
}
});
// @ts-expect-error
assert.deepEqual(child3.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 38,
column: 2,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 37,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 36,
column: 0,
parent: null
}
}
}
}
});
// @ts-expect-error
assert.deepEqual(child4.__svelte_meta.parent, {
file: 'passthrough.svelte',
type: 'render',
line: 8,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'if',
line: 7,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 43,
column: 1,
parent: {
file: 'main.svelte',
type: 'if',
line: 42,
column: 0,
parent: null
}
}
}
});
// @ts-expect-error
assert.deepEqual(dynamic.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'x.y',
line: 50,
column: 0,
parent: null
});
}
await tick();
check();
// Test that stack is kept when re-rendering
const button = target.querySelector('button');
button?.click();
await tick();
button?.click();
await tick();
check();
}
});

@ -0,0 +1,50 @@
<script>
import Child from "./child.svelte";
import Passthrough from "./passthrough.svelte";
let x = { y: Child }
let key = 'test';
let show = $state(true);
</script>
<p>no parent</p>
<button onclick={() => show = !show}>toggle</button>
{#if true}
<p>if</p>
{/if}
{#each [1]}
<p>each</p>
{/each}
{#await Promise.resolve()}
<p>loading</p>
{:then}
<p>await</p>
{/await}
{#key key}
<p>key</p>
{/key}
<Child />
<Passthrough>
<Child />
</Passthrough>
<Passthrough>
<Passthrough>
<Child />
</Passthrough>
</Passthrough>
{#if show}
<Passthrough>
{#snippet named()}
<p>hi</p>
{/snippet}
</Passthrough>
{/if}
<x.y />

@ -0,0 +1,9 @@
<script>
let { children, named } = $props();
</script>
{@render children?.()}
{#if true}
{@render named?.()}
{/if}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const [btn1, btn2, btn3] = target.getElementsByTagName('button');
const [div] = target.getElementsByTagName('div');
flushSync(() => btn1.click());
assert.equal(div.textContent, '1');
flushSync(() => btn2.click());
assert.equal(div.textContent, '1');
flushSync(() => btn3.click());
assert.equal(div.textContent, '2');
flushSync(() => btn1.click());
assert.equal(div.textContent, '2');
flushSync(() => btn2.click());
assert.equal(div.textContent, '2');
flushSync(() => btn3.click());
assert.equal(div.textContent, '3');
}
});

@ -0,0 +1,9 @@
<script>
let count = $derived({ value: 1 });
</script>
<button onclick={() => count.value++}>mutate</button>
<button onclick={() => count = count}>assign self</button>
<button onclick={() => count = {...count} }>assign copy</button>
<div>{count.value}</div>

@ -112,6 +112,45 @@ describe('signals', () => {
};
});
test('unowned deriveds are not added as reactions but trigger effects', () => {
var obj = state<any>(undefined);
class C1 {
#v = state(0);
get v() {
return $.get(this.#v);
}
set v(v: number) {
set(this.#v, v);
}
}
return () => {
let d = derived(() => $.get(obj)?.v || '-');
const log: number[] = [];
assert.equal($.get(d), '-');
let destroy = effect_root(() => {
render_effect(() => {
log.push($.get(d));
});
});
set(obj, new C1());
flushSync();
assert.equal($.get(d), '-');
$.get(obj).v = 1;
flushSync();
assert.equal($.get(d), 1);
assert.deepEqual(log, ['-', 1]);
destroy();
// ensure we're not leaking reactions
assert.equal(obj.reactions, null);
assert.equal(d.reactions, null);
};
});
test('derived from state', () => {
const log: number[] = [];

@ -22,6 +22,7 @@ export default function Bind_component_snippet($$anchor) {
get value() {
return $.get(value);
},
set value($$value) {
$.set(value, $$value, true);
}

@ -16,6 +16,7 @@ export default function Bind_component_snippet($$payload) {
get value() {
return value;
},
set value($$value) {
value = $$value;
$$settled = false;

@ -6,6 +6,7 @@ var root = $.from_html(`<div></div> <svg></svg> <custom-element></custom-element
export default function Main($$anchor) {
// needs to be a snapshot test because jsdom does auto-correct the attribute casing
let x = 'test';
let y = () => 'test';
var fragment = root();
var div = $.first_child(fragment);

@ -3,6 +3,7 @@ import * as $ from 'svelte/internal/server';
export default function Main($$payload) {
// needs to be a snapshot test because jsdom does auto-correct the attribute casing
let x = 'test';
let y = () => 'test';
$$payload.out += `<div${$.attr('foobar', x)}></div> <svg${$.attr('viewBox', x)}></svg> <custom-element${$.attr('foobar', x)}></custom-element> <div${$.attr('foobar', y())}></div> <svg${$.attr('viewBox', y())}></svg> <custom-element${$.attr('foobar', y())}></custom-element>`;

@ -14,6 +14,7 @@ export default function Function_prop_no_getter($$anchor) {
onmousedown: () => $.set(count, $.get(count) + 1),
onmouseup,
onmouseenter: () => $.set(count, plusOne($.get(count)), true),
children: ($$anchor, $$slotProps) => {
$.next();
@ -22,6 +23,7 @@ export default function Function_prop_no_getter($$anchor) {
$.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ''}`));
$.append($$anchor, text);
},
$$slots: { default: true }
});
}

@ -13,9 +13,11 @@ export default function Function_prop_no_getter($$payload) {
onmousedown: () => count += 1,
onmouseup,
onmouseenter: () => count = plusOne(count),
children: ($$payload) => {
$$payload.out += `<!---->clicks: ${$.escape(count)}`;
},
$$slots: { default: true }
});
}

@ -6,6 +6,7 @@ var root = $.from_tree(
[
['h1', null, 'hello'],
' ',
[
'div',
{ class: 'potato' },

@ -3,6 +3,4 @@ import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
import { random } from './module.svelte';
export default function Imports_in_modules($$anchor) {
}
export default function Imports_in_modules($$anchor) {}

@ -1,6 +1,4 @@
import * as $ from 'svelte/internal/server';
import { random } from './module.svelte';
export default function Imports_in_modules($$payload) {
}
export default function Imports_in_modules($$payload) {}

@ -348,6 +348,30 @@ declare module 'svelte' {
*/
props: Props;
});
/**
* Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed.
*
* Must be called while a derived or effect is running.
*
* ```svelte
* <script>
* import { getAbortSignal } from 'svelte';
*
* let { id } = $props();
*
* async function getData(id) {
* const response = await fetch(`/items/${id}`, {
* signal: getAbortSignal()
* });
*
* return await response.json();
* }
*
* const data = $derived(await getData(id));
* </script>
* ```
*/
export function getAbortSignal(): AbortSignal;
/**
* `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM.
* Unlike `$effect`, the provided function only runs once.
@ -1120,6 +1144,8 @@ declare module 'svelte/compiler' {
instance: Script | null;
/** The parsed `<script module>` element, if exists */
module: Script | null;
/** Comments found in <script> and {expressions} */
comments: JSComment[];
}
export interface SvelteOptions {
@ -1437,6 +1463,17 @@ declare module 'svelte/compiler' {
attributes: Attribute[];
}
export interface JSComment {
type: 'Line' | 'Block';
value: string;
start: number;
end: number;
loc: {
start: { line: number; column: number };
end: { line: number; column: number };
};
}
export type AttributeLike = Attribute | SpreadAttribute | Directive;
export type Directive =
@ -1493,7 +1530,7 @@ declare module 'svelte/compiler' {
| AST.Comment
| Block;
export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node;
export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node | Script;
export type { _CSS as CSS };
}

@ -0,0 +1,5 @@
export default {
compilerOptions: {
hmr: true
}
};

@ -7,14 +7,7 @@ export default defineConfig({
minify: false
},
plugins: [
inspect(),
svelte({
compilerOptions: {
hmr: true
}
})
],
plugins: [inspect(), svelte()],
optimizeDeps: {
// svelte is a local workspace package, optimizing it would require dev server restarts with --force for every change

@ -87,8 +87,8 @@ importers:
specifier: ^1.2.1
version: 1.2.1
esrap:
specifier: ^1.4.8
version: 1.4.8
specifier: ^2.1.0
version: 2.1.0
is-reference:
specifier: ^3.0.3
version: 3.0.3
@ -1261,8 +1261,8 @@ packages:
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
engines: {node: '>=0.10'}
esrap@1.4.8:
resolution: {integrity: sha512-jlENbjZ7lqgJV9/OmgAtVqrFFMwsl70ctOgPIg5oTdQVGC13RSkMdtvAmu7ZTLax92c9ljnIG0xleEkdL69hwg==}
esrap@2.1.0:
resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
@ -3622,7 +3622,7 @@ snapshots:
dependencies:
estraverse: 5.3.0
esrap@1.4.8:
esrap@2.1.0:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0

Loading…
Cancel
Save