diff --git a/.changeset/new-trees-behave.md b/.changeset/new-trees-behave.md new file mode 100644 index 0000000000..d5fab30f3e --- /dev/null +++ b/.changeset/new-trees-behave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify props diff --git a/.changeset/short-fireants-flow.md b/.changeset/short-fireants-flow.md new file mode 100644 index 0000000000..b9955ff577 --- /dev/null +++ b/.changeset/short-fireants-flow.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `getAbortSignal()` diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 111b0b8940..3f33e37d2e 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -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 ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 6d96770eba..47c2038d70 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1d26663143..2d88d2a051 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -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.0.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", diff --git a/packages/svelte/scripts/process-messages/index.js b/packages/svelte/scripts/process-messages/index.js index 81c59271de..e705a1c921 100644 --- a/packages/svelte/scripts/process-messages/index.js +++ b/packages/svelte/scripts/process-messages/index.js @@ -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' ); } diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 25e72340c6..e9fa33b054 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -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`); } /** diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 756a88a824..9ba23c1485 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -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} */ +// 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); diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index f6b7e4b054..85345bca4a 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -451,6 +451,7 @@ export function convert(source, ast) { SpreadAttribute(node) { return { ...node, type: 'Spread' }; }, + // @ts-ignore StyleSheet(node, context) { return { ...node, diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 5ca9adb98b..fdc9734f85 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -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); diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 26a09abb66..77ce4a461c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -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; diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 6cc5b58aa6..77cc2bf3fa 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -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; } diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index b118901830..282288e2a2 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -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 diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js index a596cdf572..5d21f85792 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js @@ -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]; diff --git a/packages/svelte/src/compiler/phases/1-parse/read/script.js b/packages/svelte/src/compiler/phases/1-parse/read/script.js index 6290127811..9ce449f200 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/script.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js @@ -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); } diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 5d77d6a8f4..ba091ef7ec 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -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: [] }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 80adc10c1a..d73e273ec2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -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) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index e2e006c14b..e85a35cf8e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -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) { diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index f96fd64ec7..33665097ab 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -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 diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 7a3d6bef6c..86346b864c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -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( diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 8fa4bff619..dba2559a17 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -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>; @@ -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; /** diff --git a/packages/svelte/src/compiler/state.js b/packages/svelte/src/compiler/state.js index 1db3db917f..9095651ced 100644 --- a/packages/svelte/src/compiler/state.js +++ b/packages/svelte/src/compiler/state.js @@ -16,6 +16,8 @@ export let warnings = []; */ export let filename; +export let component_name = ''; + /** * The original source code * @type {string} @@ -28,8 +30,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 +81,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 +96,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(); } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 2a7ec7b5c6..c9841e9d4e 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -72,6 +72,8 @@ export namespace AST { instance: Script | null; /** The parsed ` + * ``` + */ +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. diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 0f1aff8f5a..e5039cf150 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -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'; diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 3ca915f98e..cd5e0d2244 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -15,8 +15,6 @@ 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; @@ -27,6 +25,12 @@ 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.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 TEXT_NODE = 3; export const COMMENT_NODE = 8; diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 962593b48d..5c3f5340e1 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -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`); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index e9cea0df3e..d3123d24a1 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -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) { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 03d073781d..a2806bde81 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -32,7 +32,8 @@ import { HEAD_EFFECT, MAYBE_DIRTY, EFFECT_HAS_DERIVED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + STALE_REACTION } from '#client/constants'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -106,7 +107,8 @@ function create_effect(type, fn, sync, push = true) { prev: null, teardown: null, transitions: null, - wv: 0 + wv: 0, + ac: null }; if (DEV) { @@ -397,6 +399,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 +482,7 @@ export function destroy_effect(effect, remove_dom = true) { effect.fn = effect.nodes_start = effect.nodes_end = + effect.ac = null; } diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index f3111361c0..a7f9daf34c 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -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,84 @@ 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; + + 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(); - prop_value = get_fallback(); - if (setter) setter(prop_value); + 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); + var value = /** @type {V} */ (props[key]); if (value !== undefined) 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 +346,39 @@ 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); - } + // prop is written to, but there's no binding, which means we + // create a derived that we can write to locally + 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); }; } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 756bb98f09..88c84f27fe 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -40,6 +40,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 extends Value, Reaction { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a8dc6904d8..4fa40bd783 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -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'; @@ -276,6 +276,11 @@ export function update_reaction(reaction) { 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; @@ -868,17 +873,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); } } diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index e07892a4b0..74f9041b91 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -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`); } diff --git a/packages/svelte/src/internal/server/abort-signal.js b/packages/svelte/src/internal/server/abort-signal.js new file mode 100644 index 0000000000..da579b2592 --- /dev/null +++ b/packages/svelte/src/internal/server/abort-signal.js @@ -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; +} diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index 38c545c84e..e47530c9aa 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -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; } \ No newline at end of file diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 2ca85fff44..ceb516ebb0 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -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 += ``; - } + for (const { hash, code } of payload.css) { + head += ``; + } - return { - head, - html: payload.out, - body: payload.out - }; + return { + head, + html: payload.out, + body: payload.out + }; + } finally { + abort(); + } } /** diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index b8606fbf6f..6bcc35016a 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -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 \`\` 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`); diff --git a/packages/svelte/src/internal/shared/warnings.js b/packages/svelte/src/internal/shared/warnings.js index 281be08382..0acca44184 100644 --- a/packages/svelte/src/internal/shared/warnings.js +++ b/packages/svelte/src/internal/shared/warnings.js @@ -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`); } diff --git a/packages/svelte/tests/parser-modern/test.ts b/packages/svelte/tests/parser-modern/test.ts index b47d4a4879..279ba7bc08 100644 --- a/packages/svelte/tests/parser-modern/test.ts +++ b/packages/svelte/tests/parser-modern/test.ts @@ -21,6 +21,8 @@ const { test, run } = suite(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')); diff --git a/packages/svelte/tests/runtime-runes/samples/get-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/_config.js new file mode 100644 index 0000000000..6a85e2615a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/_config.js @@ -0,0 +1,34 @@ +import { test } from '../../test'; + +export default test({ + html: `

loading...

`, + + 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, '

0

'); + + button.click(); + await new Promise((f) => setTimeout(f, 50)); + assert.htmlEqual(target.innerHTML, '

2

'); + + assert.deepEqual(logs, [ + 'aborted', + 'StaleReactionError', + 'The reaction that called `getAbortSignal()` was re-run or destroyed' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/get-abort-signal/main.svelte b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/main.svelte new file mode 100644 index 0000000000..be5625125b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/main.svelte @@ -0,0 +1,33 @@ + + + + +{#await delayed_count} +

loading...

+{:then count} +

{count}

+{:catch error} + {console.log('this should never be rendered')} +{/await} diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index ba3f4b155a..a87a356d58 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -22,6 +22,7 @@ export default function Bind_component_snippet($$anchor) { get value() { return $.get(value); }, + set value($$value) { $.set(value, $$value, true); } diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js index cadae2cf15..e2c0ee29a5 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js @@ -16,6 +16,7 @@ export default function Bind_component_snippet($$payload) { get value() { return value; }, + set value($$value) { value = $$value; $$settled = false; diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index 28bb01fb18..d84b674f88 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -6,6 +6,7 @@ var root = $.from_html(`
'test'; var fragment = root(); var div = $.first_child(fragment); diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js index 4ea5edb6a0..cf731d8187 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js @@ -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 += ` `; diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index 762a23754c..218951b836 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -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 } }); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index 88f6f55ee7..7d37abd97b 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -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 } }); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js index 792d5421e1..d4034dc55d 100644 --- a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js @@ -6,6 +6,7 @@ var root = $.from_tree( [ ['h1', null, 'hello'], ' ', + [ 'div', { class: 'potato' }, diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/index.svelte.js index ebbe191dcb..0eab38919c 100644 --- a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/index.svelte.js @@ -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) { - -} \ No newline at end of file +export default function Imports_in_modules($$anchor) {} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js index 4cd6bc59d7..2ed863d68f 100644 --- a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/index.svelte.js @@ -1,6 +1,4 @@ import * as $ from 'svelte/internal/server'; import { random } from './module.svelte'; -export default function Imports_in_modules($$payload) { - -} \ No newline at end of file +export default function Imports_in_modules($$payload) {} \ No newline at end of file diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 1a83e0d0f1..432171ae0d 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -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 + * + * ``` + */ + 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 `