Merge branch 'main' into svelte-dev-meta

pull/16255/head
Simon Holthausen 3 months ago
commit d387f7958e

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: add `getAbortSignal()`

@ -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 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 ### hydration_failed
``` ```

@ -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 > 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 ## hydration_failed
> Failed to hydrate the application > Failed to hydrate the application

@ -164,14 +164,14 @@
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.3.0", "@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5", "@types/estree": "^1.0.5",
"acorn": "^8.12.1", "acorn": "^8.12.1",
"@sveltejs/acorn-typescript": "^1.0.5",
"aria-query": "^5.3.1", "aria-query": "^5.3.1",
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"esm-env": "^1.2.1", "esm-env": "^1.2.1",
"esrap": "^1.4.8", "esrap": "^2.0.0",
"is-reference": "^3.0.3", "is-reference": "^3.0.3",
"locate-character": "^3.0.0", "locate-character": "^3.0.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",

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

@ -15,10 +15,12 @@ class InternalCompileError extends Error {
constructor(code, message, position) { constructor(code, message, position) {
super(message); super(message);
this.stack = ''; // avoid unnecessary noise; don't set it as a class property or it becomes enumerable 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. // 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 // 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. // we create an instance of the shared class an copy over its properties.
this.#diagnostic = new CompileDiagnostic(code, message, position); this.#diagnostic = new CompileDiagnostic(code, message, position);
Object.assign(this, this.#diagnostic); Object.assign(this, this.#diagnostic);
this.name = 'CompileError'; this.name = 'CompileError';
} }
@ -816,7 +818,9 @@ export function bind_invalid_expression(node) {
* @returns {never} * @returns {never}
*/ */
export function bind_invalid_name(node, name, explanation) { 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 { AST } from './public.js' */
import { walk as zimmerframe_walk } from 'zimmerframe'; import { walk as zimmerframe_walk } from 'zimmerframe';
import { convert } from './legacy.js'; 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 { parse as _parse } from './phases/1-parse/index.js';
import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js'; import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js';
import { analyze_component, analyze_module } from './phases/2-analyze/index.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) { export function compile(source, options) {
source = remove_bom(source); source = remove_bom(source);
state.reset_warning_filter(options.warningFilter); state.reset_warnings(options.warningFilter);
const validated = validate_component_options(options, ''); const validated = validate_component_options(options, '');
state.reset(source, validated);
let parsed = _parse(source); let parsed = _parse(source);
@ -65,11 +63,10 @@ export function compile(source, options) {
*/ */
export function compileModule(source, options) { export function compileModule(source, options) {
source = remove_bom(source); source = remove_bom(source);
state.reset_warning_filter(options.warningFilter); state.reset_warnings(options.warningFilter);
const validated = validate_module_options(options, ''); 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); return transform_module(analysis, source, validated);
} }
@ -97,6 +94,7 @@ export function compileModule(source, options) {
* @returns {Record<string, any>} * @returns {Record<string, any>}
*/ */
// TODO 6.0 remove unused `filename`
/** /**
* The parse function parses a component, returning only its abstract syntax tree. * 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 `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 {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options] * @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
* @returns {AST.Root | LegacyRoot} * @returns {AST.Root | LegacyRoot}
*/ */
export function parse(source, { filename, rootDir, modern, loose } = {}) { export function parse(source, { modern, loose } = {}) {
source = remove_bom(source); source = remove_bom(source);
state.reset_warning_filter(() => false); state.reset_warnings(() => false);
state.reset(source, { filename: filename ?? '(unknown)', rootDir });
const ast = _parse(source, loose); const ast = _parse(source, loose);
return to_public_ast(source, ast, modern); return to_public_ast(source, ast, modern);

@ -451,6 +451,7 @@ export function convert(source, ast) {
SpreadAttribute(node) { SpreadAttribute(node) {
return { ...node, type: 'Spread' }; return { ...node, type: 'Spread' };
}, },
// @ts-ignore
StyleSheet(node, context) { StyleSheet(node, context) {
return { return {
...node, ...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 { regex_valid_component_name } from '../phases/1-parse/state/element.js';
import { analyze_component } from '../phases/2-analyze/index.js'; import { analyze_component } from '../phases/2-analyze/index.js';
import { get_rune } from '../phases/scope.js'; import { get_rune } from '../phases/scope.js';
import { reset, reset_warning_filter } from '../state.js'; import { reset, reset_warnings } from '../state.js';
import { import {
extract_identifiers, extract_identifiers,
extract_all_identifiers_from_expression, extract_all_identifiers_from_expression,
@ -134,8 +134,7 @@ export function migrate(source, { filename, use_ts } = {}) {
return start + style_placeholder + end; return start + style_placeholder + end;
}); });
reset_warning_filter(() => false); reset_warnings(() => false);
reset(source, { filename: filename ?? '(unknown)' });
let parsed = parse(source); let parsed = parse(source);

@ -1,18 +1,32 @@
/** @import { Comment, Program } from 'estree' */ /** @import { Comment, Program } from 'estree' */
/** @import { AST } from '#compiler' */
import * as acorn from 'acorn'; import * as acorn from 'acorn';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript'; import { tsPlugin } from '@sveltejs/acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin()); const ParserWithTS = acorn.Parser.extend(tsPlugin());
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** /**
* @param {string} source * @param {string} source
* @param {AST.JSComment[]} comments
* @param {boolean} typescript * @param {boolean} typescript
* @param {boolean} [is_script] * @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 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 // @ts-ignore
const parse_statement = parser.prototype.parseStatement; const parse_statement = parser.prototype.parseStatement;
@ -53,13 +67,19 @@ export function parse(source, typescript, is_script) {
/** /**
* @param {string} source * @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript * @param {boolean} typescript
* @param {number} index * @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} * @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 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, { const ast = parser.parseExpressionAt(source, index, {
onComment, 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 * 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. * in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source * @param {string} source
* @param {CommentWithLocation[]} comments
* @param {number} index
*/ */
function get_comment_handlers(source) { function get_comment_handlers(source, comments, index = 0) {
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** @type {CommentWithLocation[]} */
const comments = [];
return { return {
/** /**
* @param {boolean} block * @param {boolean} block
* @param {string} value * @param {string} value
* @param {number} start * @param {number} start
* @param {number} end * @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)) { if (block && /\n/.test(value)) {
let a = start; let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1; 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'), ''); 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 */ /** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) { add_comments(ast) {
if (comments.length === 0) return; if (comments.length === 0) return;
comments = comments
.filter((comment) => comment.start >= index)
.map(({ type, value, start, end }) => ({ type, value, start, end }));
walk(ast, null, { walk(ast, null, {
_(node, { next, path }) { _(node, { next, path }) {
let comment; let comment;

@ -1,4 +1,5 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Comment } from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use // @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js'; 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 read_options from './read/options.js';
import { is_reserved } from '../../../utils.js'; import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js'; import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/; const regex_position_indicator = / \(\d+:\d+\)$/;
@ -87,6 +89,7 @@ export class Parser {
type: 'Root', type: 'Root',
fragment: create_fragment(), fragment: create_fragment(),
options: null, options: null,
comments: [],
metadata: { metadata: {
ts: this.ts ts: this.ts
} }
@ -299,6 +302,8 @@ export class Parser {
* @returns {AST.Root} * @returns {AST.Root}
*/ */
export function parse(template, loose = false) { export function parse(template, loose = false) {
state.set_source(template);
const parser = new Parser(template, loose); const parser = new Parser(template, loose);
return parser.root; 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); space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ ( 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; ).left;
expression.typeAnnotation = read_type_annotation(parser); 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 as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error. // parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':'); 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 // `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') { if (expression.type === 'AssignmentExpression') {
let b = expression.right.start; let b = expression.right.start;
while (template[b] !== '=') b -= 1; 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 // `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) { export default function read_expression(parser, opening_token, disallow_loose) {
try { 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; let num_parens = 0;
if (node.leadingComments !== undefined && node.leadingComments.length > 0) { let i = parser.root.comments.length;
parser.index = node.leadingComments.at(-1).end; 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) { 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); 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) { while (num_parens > 0) {
const char = parser.template[index]; const char = parser.template[index];

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

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

@ -362,6 +362,9 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (template.body) .../** @type {ESTree.Statement[]} */ (template.body)
]); ]);
// trick esrap into including comments
component_block.loc = instance.loc;
if (!analysis.runes) { if (!analysis.runes) {
// Bind static exports to props so that people can access them with bind:x // Bind static exports to props so that people can access them with bind:x
for (const { name, alias } of analysis.exports) { for (const { name, alias } of analysis.exports) {

@ -1,6 +1,8 @@
/** @import { Node } from 'esrap/languages/ts' */
/** @import { ValidatedCompileOptions, CompileResult, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { ValidatedCompileOptions, CompileResult, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentAnalysis, Analysis } from '../types' */ /** @import { ComponentAnalysis, Analysis } from '../types' */
import { print } from 'esrap'; import { print } from 'esrap';
import ts from 'esrap/languages/ts';
import { VERSION } from '../../../version.js'; import { VERSION } from '../../../version.js';
import { server_component, server_module } from './server/transform-server.js'; import { server_component, server_module } from './server/transform-server.js';
import { client_component, client_module } from './client/transform-client.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_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 // 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) // (else esrap does return null for source and sourceMapContent which may trip up tooling)
sourceMapContent: source, sourceMapContent: source,
@ -93,13 +95,19 @@ export function transform_module(analysis, source, options) {
]; ];
} }
return { const js = print(/** @type {Node} */ (program), ts({ comments: analysis.comments }), {
js: print(program, {
// include source content; makes it easier/more robust looking up the source map code // 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) // (else esrap does return null for source and sourceMapContent which may trip up tooling)
sourceMapContent: source, sourceMapContent: source,
sourceMapSource: get_source_name(options.filename, undefined, 'input.svelte.js') 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,
css: null, css: null,
metadata: { metadata: {
runes: true runes: true

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

@ -2,6 +2,7 @@ import type { AST, Binding, StateField } from '#compiler';
import type { import type {
AssignmentExpression, AssignmentExpression,
ClassBody, ClassBody,
Comment,
Identifier, Identifier,
LabeledStatement, LabeledStatement,
Node, Node,
@ -33,10 +34,13 @@ export interface ReactiveStatement {
*/ */
export interface Analysis { export interface Analysis {
module: Js; 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` 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; runes: boolean;
immutable: boolean; immutable: boolean;
tracing: boolean; tracing: boolean;
comments: AST.JSComment[];
classes: Map<ClassBody, Map<string, StateField>>; classes: Map<ClassBody, Map<string, StateField>>;
@ -88,6 +92,7 @@ export interface ComponentAnalysis extends Analysis {
keyframes: string[]; keyframes: string[];
has_global: boolean; has_global: boolean;
}; };
/** @deprecated use `source` from `state.js` instead */
source: string; source: string;
undefined_exports: Map<string, Node>; undefined_exports: Map<string, Node>;
/** /**

@ -16,6 +16,8 @@ export let warnings = [];
*/ */
export let filename; export let filename;
export let component_name = '<unknown>';
/** /**
* The original source code * The original source code
* @type {string} * @type {string}
@ -28,8 +30,16 @@ export let source;
*/ */
export let dev; export let dev;
export let runes = false;
export let locator = getLocator('', { offsetLine: 1 }); 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 * @param {AST.SvelteNode & { start?: number | undefined }} node
*/ */
@ -71,8 +81,9 @@ export function pop_ignore() {
* *
* @param {(warning: Warning) => boolean} fn * @param {(warning: Warning) => boolean} fn
*/ */
export function reset_warning_filter(fn = () => true) { export function reset_warnings(fn = () => true) {
warning_filter = fn; warning_filter = fn;
warnings = [];
} }
/** /**
@ -85,23 +96,27 @@ export function is_ignored(node, code) {
} }
/** /**
* @param {string} _source * @param {{
* @param {{ dev?: boolean; filename: string; rootDir?: string }} options * dev: boolean;
* filename: string;
* component_name?: string;
* rootDir?: string;
* runes: boolean;
* }} state
*/ */
export function reset(_source, options) { export function reset(state) {
source = _source; const root_dir = state.rootDir?.replace(/\\/g, '/');
const root_dir = options.rootDir?.replace(/\\/g, '/'); filename = state.filename.replace(/\\/g, '/');
filename = options.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)) { if (typeof root_dir === 'string' && filename.startsWith(root_dir)) {
// make filename relative to rootDir // make filename relative to rootDir
filename = filename.replace(root_dir, '').replace(/^[/\\]/, ''); filename = filename.replace(root_dir, '').replace(/^[/\\]/, '');
} }
locator = getLocator(source, { offsetLine: 1 });
warnings = [];
ignore_stack = []; ignore_stack = [];
ignore_map.clear(); ignore_map.clear();
} }

@ -72,6 +72,8 @@ export namespace AST {
instance: Script | null; instance: Script | null;
/** The parsed `<script module>` element, if exists */ /** The parsed `<script module>` element, if exists */
module: Script | null; module: Script | null;
/** Comments found in <script> and {expressions} */
comments: JSComment[];
/** @internal */ /** @internal */
metadata: { metadata: {
/** Whether the component was parsed with typescript */ /** Whether the component was parsed with typescript */
@ -537,6 +539,17 @@ export namespace AST {
attributes: Attribute[]; 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 AttributeLike = Attribute | SpreadAttribute | Directive;
export type Directive = export type Directive =
@ -593,7 +606,7 @@ export namespace AST {
| AST.Comment | AST.Comment
| Block; | 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 }; 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 // map may contain a different file name. Patch our map beforehand to align sources so merging
// with the preprocessor map works correctly. // with the preprocessor map works correctly.
result.map.sources = [file_basename]; result.map.sources = [file_basename];
result.map = apply_preprocessor_sourcemap( Object.assign(
result.map,
apply_preprocessor_sourcemap(
file_basename, file_basename,
result.map, result.map,
/** @type {any} */ (options.sourcemap) /** @type {any} */ (options.sourcemap)
)
); );
// After applying the preprocessor map, we need to do the inverse and make the sources // 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. // 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! */ /* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { import { warnings, ignore_stack, ignore_map, warning_filter } from './state.js';
warnings,
ignore_stack,
ignore_map,
warning_filter
} from './state.js';
import { CompileDiagnostic } from './utils/compile_diagnostic.js'; import { CompileDiagnostic } from './utils/compile_diagnostic.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */ /** @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); const warning = new InternalCompileWarning(code, message, node && node.start !== undefined ? [node.start, node.end ?? node.start] : undefined);
if (!warning_filter(warning)) return; if (!warning_filter(warning)) return;
warnings.push(warning); warnings.push(warning);
} }
@ -496,7 +491,9 @@ export function a11y_role_supports_aria_props_implicit(node, attribute, role, na
* @param {string | undefined | null} [suggestion] * @param {string | undefined | null} [suggestion]
*/ */
export function a11y_unknown_aria_attribute(node, attribute, 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] * @param {string | undefined | null} [suggestion]
*/ */
export function a11y_unknown_role(node, role, 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] * @param {string | undefined | null} [suggestion]
*/ */
export function unknown_code(node, code, 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 { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */ /** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.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 { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js'; import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js'; import * as e from './internal/client/errors.js';
@ -44,6 +44,37 @@ if (DEV) {
throw_rune_error('$bindable'); 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. * `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. * Unlike `$effect`, the provided function only runs once.

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

@ -27,6 +27,12 @@ export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol(''); export const LOADING_ATTR_SYMBOL = Symbol('');
export const PROXY_PATH_SYMBOL = Symbol('proxy path'); export const PROXY_PATH_SYMBOL = Symbol('proxy path');
// allow users to ignore aborted signal errors if `reason.stale`
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 ELEMENT_NODE = 1;
export const TEXT_NODE = 3; export const TEXT_NODE = 3;
export const COMMENT_NODE = 8; export const COMMENT_NODE = 8;

@ -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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/bind_invalid_checkbox_value`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/bind_invalid_export`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/bind_not_bindable`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/component_api_changed`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/component_api_invalid_new`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/derived_references_self`); 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) { export function each_key_duplicate(a, b, value) {
if (DEV) { 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/each_key_duplicate`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/effect_in_teardown`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/effect_in_unowned_derived`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/effect_orphan`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/effect_update_depth_exceeded`); 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 * Failed to hydrate the application
* @returns {never} * @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`); const error = new Error(`hydration_failed\nFailed to hydrate the application\nhttps://svelte.dev/e/hydration_failed`);
error.name = 'Svelte error'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/hydration_failed`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/invalid_snippet`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/lifecycle_legacy_only`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/props_invalid_value`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/props_rest_readonly`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/rune_outside_svelte`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/state_descriptors_fixed`); 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`); const error = new Error(`state_prototype_fixed\nCannot set prototype of \`$state\` object\nhttps://svelte.dev/e/state_prototype_fixed`);
error.name = 'Svelte error'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/state_prototype_fixed`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/state_unsafe_mutation`); throw new Error(`https://svelte.dev/e/state_unsafe_mutation`);

@ -53,7 +53,8 @@ export function derived(fn) {
rv: 0, rv: 0,
v: /** @type {V} */ (null), v: /** @type {V} */ (null),
wv: 0, wv: 0,
parent: parent_derived ?? active_effect parent: parent_derived ?? active_effect,
ac: null
}; };
if (DEV && tracing_mode_flag) { if (DEV && tracing_mode_flag) {

@ -32,7 +32,8 @@ import {
HEAD_EFFECT, HEAD_EFFECT,
MAYBE_DIRTY, MAYBE_DIRTY,
EFFECT_HAS_DERIVED, EFFECT_HAS_DERIVED,
BOUNDARY_EFFECT BOUNDARY_EFFECT,
STALE_REACTION
} from '#client/constants'; } from '#client/constants';
import { set } from './sources.js'; import { set } from './sources.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
@ -106,7 +107,8 @@ function create_effect(type, fn, sync, push = true) {
prev: null, prev: null,
teardown: null, teardown: null,
transitions: null, transitions: null,
wv: 0 wv: 0,
ac: null
}; };
if (DEV) { if (DEV) {
@ -397,6 +399,8 @@ export function destroy_effect_children(signal, remove_dom = false) {
signal.first = signal.last = null; signal.first = signal.last = null;
while (effect !== null) { while (effect !== null) {
effect.ac?.abort(STALE_REACTION);
var next = effect.next; var next = effect.next;
if ((effect.f & ROOT_EFFECT) !== 0) { if ((effect.f & ROOT_EFFECT) !== 0) {
@ -478,6 +482,7 @@ export function destroy_effect(effect, remove_dom = true) {
effect.fn = effect.fn =
effect.nodes_start = effect.nodes_start =
effect.nodes_end = effect.nodes_end =
effect.ac =
null; null;
} }

@ -40,6 +40,8 @@ export interface Reaction extends Signal {
fn: null | Function; fn: null | Function;
/** Signals that this signal reads from */ /** Signals that this signal reads from */
deps: null | Value[]; deps: null | Value[];
/** An AbortController that aborts when the signal is destroyed */
ac: null | AbortController;
} }
export interface Derived<V = unknown> extends Value<V>, Reaction { export interface Derived<V = unknown> extends Value<V>, Reaction {

@ -22,7 +22,8 @@ import {
ROOT_EFFECT, ROOT_EFFECT,
LEGACY_DERIVED_PROP, LEGACY_DERIVED_PROP,
DISCONNECTED, DISCONNECTED,
EFFECT_IS_UPDATING EFFECT_IS_UPDATING,
STALE_REACTION
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js'; import { flush_tasks } from './dom/task.js';
import { internal_set, old_values } from './reactivity/sources.js'; import { internal_set, old_values } from './reactivity/sources.js';
@ -276,6 +277,11 @@ export function update_reaction(reaction) {
reaction.f |= EFFECT_IS_UPDATING; reaction.f |= EFFECT_IS_UPDATING;
if (reaction.ac !== null) {
reaction.ac.abort(STALE_REACTION);
reaction.ac = null;
}
try { try {
var result = /** @type {Function} */ (0, reaction.fn)(); var result = /** @type {Function} */ (0, reaction.fn)();
var deps = reaction.deps; var deps = reaction.deps;

@ -25,7 +25,13 @@ export function assignment_value_stale(property, location) {
*/ */
export function binding_property_non_reactive(binding, location) { export function binding_property_non_reactive(binding, location) {
if (DEV) { 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 { } else {
console.warn(`https://svelte.dev/e/binding_property_non_reactive`); 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) { export function hydration_html_changed(location) {
if (DEV) { 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 { } else {
console.warn(`https://svelte.dev/e/hydration_html_changed`); console.warn(`https://svelte.dev/e/hydration_html_changed`);
} }
@ -88,7 +100,13 @@ export function hydration_html_changed(location) {
*/ */
export function hydration_mismatch(location) { export function hydration_mismatch(location) {
if (DEV) { 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 { } else {
console.warn(`https://svelte.dev/e/hydration_mismatch`); 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! */ /* This file is generated by scripts/process-messages/index.js. Do not edit! */
/** /**
* `%name%(...)` is not available on the server * `%name%(...)` is not available on the server
* @param {string} name * @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`); 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'; error.name = 'Svelte error';
throw 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 { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import { reset_elements } from './dev.js'; import { reset_elements } from './dev.js';
import { Payload } from './payload.js'; import { Payload } from './payload.js';
import { abort } from './abort-signal.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter // https://infra.spec.whatwg.org/#noncharacter
@ -66,6 +67,7 @@ export let on_destroy = [];
* @returns {RenderOutput} * @returns {RenderOutput}
*/ */
export function render(component, options = {}) { export function render(component, options = {}) {
try {
const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : ''); const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
const prev_on_destroy = on_destroy; const prev_on_destroy = on_destroy;
@ -110,6 +112,9 @@ export function render(component, options = {}) {
html: payload.out, html: payload.out,
body: 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/invalid_default_snippet`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/lifecycle_outside_component`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/snippet_without_render_tag`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/store_invalid_shape`); 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`); 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'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
throw new Error(`https://svelte.dev/e/svelte_element_invalid_this_value`); 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) { export function state_snapshot_uncloneable(properties) {
if (DEV) { if (DEV) {
console.warn(`%c[svelte] state_snapshot_uncloneable\n%c${properties 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: ? `The following properties cannot be cloned with \`$state.snapshot\` — the return value contains the originals:
${properties}` ${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 { } else {
console.warn(`https://svelte.dev/e/state_snapshot_uncloneable`); console.warn(`https://svelte.dev/e/state_snapshot_uncloneable`);
} }

@ -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 // run `UPDATE_SNAPSHOTS=true pnpm test parser` to update parser tests
if (process.env.UPDATE_SNAPSHOTS) { if (process.env.UPDATE_SNAPSHOTS) {
fs.writeFileSync(`${cwd}/output.json`, JSON.stringify(actual, null, '\t')); fs.writeFileSync(`${cwd}/output.json`, JSON.stringify(actual, null, '\t'));

@ -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}

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

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

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

@ -3,6 +3,7 @@ import * as $ from 'svelte/internal/server';
export default function Main($$payload) { export default function Main($$payload) {
// needs to be a snapshot test because jsdom does auto-correct the attribute casing // needs to be a snapshot test because jsdom does auto-correct the attribute casing
let x = 'test'; let x = 'test';
let y = () => '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>`; $$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), onmousedown: () => $.set(count, $.get(count) + 1),
onmouseup, onmouseup,
onmouseenter: () => $.set(count, plusOne($.get(count)), true), onmouseenter: () => $.set(count, plusOne($.get(count)), true),
children: ($$anchor, $$slotProps) => { children: ($$anchor, $$slotProps) => {
$.next(); $.next();
@ -22,6 +23,7 @@ export default function Function_prop_no_getter($$anchor) {
$.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ''}`)); $.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ''}`));
$.append($$anchor, text); $.append($$anchor, text);
}, },
$$slots: { default: true } $$slots: { default: true }
}); });
} }

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

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

@ -3,6 +3,4 @@ import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client'; import * as $ from 'svelte/internal/client';
import { random } from './module.svelte'; 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 * as $ from 'svelte/internal/server';
import { random } from './module.svelte'; 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; 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. * `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. * Unlike `$effect`, the provided function only runs once.
@ -1120,6 +1144,8 @@ declare module 'svelte/compiler' {
instance: Script | null; instance: Script | null;
/** The parsed `<script module>` element, if exists */ /** The parsed `<script module>` element, if exists */
module: Script | null; module: Script | null;
/** Comments found in <script> and {expressions} */
comments: JSComment[];
} }
export interface SvelteOptions { export interface SvelteOptions {
@ -1437,6 +1463,17 @@ declare module 'svelte/compiler' {
attributes: Attribute[]; 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 AttributeLike = Attribute | SpreadAttribute | Directive;
export type Directive = export type Directive =
@ -1493,7 +1530,7 @@ declare module 'svelte/compiler' {
| AST.Comment | AST.Comment
| Block; | 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 }; export type { _CSS as CSS };
} }

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

@ -7,14 +7,7 @@ export default defineConfig({
minify: false minify: false
}, },
plugins: [ plugins: [inspect(), svelte()],
inspect(),
svelte({
compilerOptions: {
hmr: true
}
})
],
optimizeDeps: { optimizeDeps: {
// svelte is a local workspace package, optimizing it would require dev server restarts with --force for every change // 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 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
esrap: esrap:
specifier: ^1.4.8 specifier: ^2.0.0
version: 1.4.8 version: 2.0.0
is-reference: is-reference:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
@ -1261,8 +1261,8 @@ packages:
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
esrap@1.4.8: esrap@2.0.0:
resolution: {integrity: sha512-jlENbjZ7lqgJV9/OmgAtVqrFFMwsl70ctOgPIg5oTdQVGC13RSkMdtvAmu7ZTLax92c9ljnIG0xleEkdL69hwg==} resolution: {integrity: sha512-zhw1TDqno99Ld5wOpe0t47rzVyxfGc1fvxNzPsqk4idUf5dcAePkAyfTceLJaSTytjiWDu26S5tI+grjvymXJA==}
esrecurse@4.3.0: esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
@ -3622,7 +3622,7 @@ snapshots:
dependencies: dependencies:
estraverse: 5.3.0 estraverse: 5.3.0
esrap@1.4.8: esrap@2.0.0:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0

Loading…
Cancel
Save