diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 17a3e980c423..6933ecafbe02 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 81c59271de2e..e705a1c921a1 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 25e72340c64d..e9fa33b054cb 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 756a88a824b6..11db09193607 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -11,6 +11,7 @@ import { transform_component, transform_module } from './phases/3-transform/inde import { validate_component_options, validate_module_options } from './validate-options.js'; import * as state from './state.js'; export { default as preprocess } from './preprocess/index.js'; +export { print } from './print/index.js'; /** * `compile` converts your `.svelte` source code into a JavaScript module that exports a component @@ -69,7 +70,7 @@ export function compileModule(source, 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); } diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index f6b7e4b0548d..85345bca4a22 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/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 26a09abb66b7..77ce4a461c2c 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 6cc5b58aa666..b8ae8199ebc4 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'; @@ -87,6 +88,7 @@ export class Parser { type: 'Root', fragment: create_fragment(), options: null, + comments: [], metadata: { ts: this.ts } 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 b1189018306c..282288e2a22f 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 a596cdf572cb..5d21f85792b0 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 629012781188..9ce449f20074 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 5d77d6a8f4b6..ba091ef7ec41 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 80adc10c1ab7..be1f1ee5bb94 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'; @@ -231,11 +232,16 @@ 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 = []; + + 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,6 +265,7 @@ export function analyze_module(ast, options) { runes: true, immutable: true, tracing: false, + comments, classes: new Map() }; @@ -429,6 +436,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) 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 e2e006c14bec..e85a35cf8ed9 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 f96fd64ec7a9..33665097ab6c 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 7a3d6bef6c31..86346b864c45 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 8fa4bff61943..661f363991f6 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, @@ -37,6 +38,7 @@ export interface Analysis { runes: boolean; immutable: boolean; tracing: boolean; + comments: AST.JSComment[]; classes: Map>; diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js new file mode 100644 index 000000000000..d908cc1435d5 --- /dev/null +++ b/packages/svelte/src/compiler/print/index.js @@ -0,0 +1,885 @@ +/** @import { AST } from '#compiler'; */ +/** @import { Context, Visitors } from 'esrap' */ +import * as esrap from 'esrap'; +import ts from 'esrap/languages/ts'; +import { is_void } from '../../utils.js'; + +/** + * @param {AST.SvelteNode} ast + */ +export function print(ast) { + return esrap.print( + ast, + /** @type {Visitors} */ ({ + ...ts({ comments: ast.type === 'Root' ? ast.comments : [] }), + ...visitors + }) + ); +} + +/** + * @param {Context} context + * @param {AST.SvelteNode} node + */ +function block(context, node, allow_inline = false) { + const child_context = context.new(); + child_context.visit(node); + + if (child_context.empty()) { + return; + } + + if (allow_inline && !child_context.multiline) { + context.append(child_context); + } else { + context.indent(); + context.newline(); + context.append(child_context); + context.dedent(); + context.newline(); + } +} + +/** @type {Visitors} */ +const visitors = { + Root(node, context) { + if (node.options) { + context.write(''); + } + + let started = false; + + for (const item of [node.module, node.instance, node.fragment, node.css]) { + if (!item) continue; + + if (started) { + context.margin(); + context.newline(); + } + + context.visit(item); + started = true; + } + }, + + Script(node, context) { + context.write(''); + block(context, node.content); + context.write(''); + }, + + Fragment(node, context) { + /** @type {AST.SvelteNode[][]} */ + const items = []; + + /** @type {AST.SvelteNode[]} */ + let sequence = []; + + const flush = () => { + items.push(sequence); + sequence = []; + }; + + for (let i = 0; i < node.nodes.length; i += 1) { + let child_node = node.nodes[i]; + + const prev = node.nodes[i - 1]; + const next = node.nodes[i + 1]; + + if (child_node.type === 'Text') { + child_node = { ...child_node }; // always clone, so we can safely mutate + + child_node.data = child_node.data.replace(/[^\S]+/g, ' '); + + // trim fragment + if (i === 0) { + child_node.data = child_node.data.trimStart(); + } + + if (i === node.nodes.length - 1) { + child_node.data = child_node.data.trimEnd(); + } + + if (child_node.data === '') { + continue; + } + + if (child_node.data.startsWith(' ') && prev && prev.type !== 'ExpressionTag') { + flush(); + child_node.data = child_node.data.trimStart(); + } + + if (child_node.data !== '') { + sequence.push({ ...child_node, data: child_node.data }); + + if (child_node.data.endsWith(' ') && next && next.type !== 'ExpressionTag') { + flush(); + child_node.data = child_node.data.trimStart(); + } + } + } else { + sequence.push(child_node); + } + } + + flush(); + + let multiline = false; + let width = 0; + + const child_contexts = items.map((sequence) => { + const child_context = context.new(); + + for (const node of sequence) { + child_context.visit(node); + multiline ||= child_context.multiline; + } + + width += child_context.measure(); + + return child_context; + }); + + multiline ||= width > 30; + + for (let i = 0; i < child_contexts.length; i += 1) { + const prev = child_contexts[i]; + const next = child_contexts[i + 1]; + + context.append(prev); + + if (next) { + if (prev.multiline || next.multiline) { + context.margin(); + context.newline(); + } else if (multiline) { + context.newline(); + } else { + context.write(' '); + } + } + } + }, + + AnimateDirective(node, context) { + context.write(`animate:${node.name}`); + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } + }, + + Atrule(node, context) { + context.write(`@${node.name}`); + if (node.prelude) context.write(` ${node.prelude}`); + + if (node.block) { + context.write(' '); + context.visit(node.block); + } else { + context.write(';'); + } + }, + + AttachTag(node, context) { + context.write('{@attach '); + context.visit(node.expression); + context.write('}'); + }, + + Attribute(node, context) { + context.write(node.name); + + if (node.value === true) return; + + context.write('='); + + if (Array.isArray(node.value)) { + if (node.value.length > 1 || node.value[0].type === 'Text') { + context.write('"'); + } + + for (const chunk of node.value) { + context.visit(chunk); + } + + if (node.value.length > 1 || node.value[0].type === 'Text') { + context.write('"'); + } + } else { + context.visit(node.value); + } + }, + + AwaitBlock(node, context) { + context.write(`{#await `); + context.visit(node.expression); + + if (node.pending) { + context.write('}'); + block(context, node.pending); + context.write('{:'); + } else { + context.write(' '); + } + + if (node.then) { + context.write(node.value ? 'then ' : 'then'); + if (node.value) context.visit(node.value); + context.write('}'); + + block(context, node.then); + + if (node.catch) { + context.write('{:'); + } + } + + if (node.catch) { + context.write(node.value ? 'catch ' : 'catch'); + if (node.error) context.visit(node.error); + context.write('}'); + + block(context, node.catch); + } + + context.write('{/await}'); + }, + + BindDirective(node, context) { + context.write(`bind:${node.name}`); + + if (node.expression.type === 'Identifier' && node.expression.name === node.name) { + // shorthand + return; + } + + context.write('={'); + + if (node.expression.type === 'SequenceExpression') { + context.visit(node.expression.expressions[0]); + context.write(', '); + context.visit(node.expression.expressions[1]); + } else { + context.visit(node.expression); + } + + context.write('}'); + }, + + Block(node, context) { + context.write('{'); + + if (node.children.length > 0) { + context.indent(); + context.newline(); + + let started = false; + + for (const child of node.children) { + if (started) { + context.margin(); + context.newline(); + } + + context.visit(child); + + started = true; + } + + context.dedent(); + context.newline(); + } + + context.write('}'); + }, + + ClassDirective(node, context) { + context.write(`class:${node.name}`); + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } + }, + + ClassSelector(node, context) { + context.write(`.${node.name}`); + }, + + Comment(node, context) { + context.write(''); + }, + + ComplexSelector(node, context) { + for (const selector of node.children) { + context.visit(selector); + } + }, + + Component(node, context) { + context.write(`<${node.name}`); + + for (let i = 0; i < node.attributes.length; i += 1) { + context.write(' '); + context.visit(node.attributes[i]); + } + + if (node.fragment.nodes.length > 0) { + context.write('>'); + block(context, node.fragment, true); + context.write(``); + } else { + context.write(' />'); + } + }, + + ConstTag(node, context) { + context.write('{@const '); + context.visit(node.declaration); + context.write('}'); + }, + + DebugTag(node, context) { + context.write('{@debug '); + let started = false; + for (const identifier of node.identifiers) { + if (started) { + context.write(', '); + } + context.visit(identifier); + started = true; + } + context.write('}'); + }, + + Declaration(node, context) { + context.write(`${node.property}: ${node.value};`); + }, + + EachBlock(node, context) { + context.write('{#each '); + context.visit(node.expression); + + if (node.context) { + context.write(' as '); + context.visit(node.context); + } + + if (node.index) { + context.write(`, ${node.index}`); + } + + if (node.key) { + context.write(' ('); + context.visit(node.key); + context.write(')'); + } + + context.write('}'); + + block(context, node.body); + + if (node.fallback) { + context.write('{:else}'); + context.visit(node.fallback); + } + + context.write('{/each}'); + }, + + ExpressionTag(node, context) { + context.write('{'); + context.visit(node.expression); + context.write('}'); + }, + + HtmlTag(node, context) { + context.write('{@html '); + context.visit(node.expression); + context.write('}'); + }, + + IfBlock(node, context) { + if (node.elseif) { + context.write('{:else if '); + context.visit(node.test); + context.write('}'); + + block(context, node.consequent); + } else { + context.write('{#if '); + context.visit(node.test); + context.write('}'); + + block(context, node.consequent); + } + + if (node.alternate !== null) { + if ( + !( + node.alternate.nodes.length === 1 && + node.alternate.nodes[0].type === 'IfBlock' && + node.alternate.nodes[0].elseif + ) + ) { + context.write('{:else}'); + } + + block(context, node.alternate); + } + + if (!node.elseif) { + context.write('{/if}'); + } + }, + + KeyBlock(node, context) { + context.write('{#key '); + context.visit(node.expression); + context.write('}'); + block(context, node.fragment); + context.write('{/key}'); + }, + + LetDirective(node, context) { + context.write(`let:${node.name}`); + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } + }, + + Nth(node, context) { + context.write(node.value); // TODO is this right? + }, + + OnDirective(node, context) { + context.write(`on:${node.name}`); + for (const modifier of node.modifiers) { + context.write(`|${modifier}`); + } + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } + }, + + PseudoClassSelector(node, context) { + context.write(`:${node.name}`); + + if (node.args) { + context.write('('); + + let started = false; + + for (const arg of node.args.children) { + if (started) { + context.write(', '); + } + + context.visit(arg); + + started = true; + } + + context.write(')'); + } + }, + + PseudoElementSelector(node, context) { + context.write(`::${node.name}`); + }, + + RegularElement(node, context) { + const child_context = context.new(); + + child_context.write('<' + node.name); + + for (const attribute of node.attributes) { + // TODO handle multiline + child_context.write(' '); + child_context.visit(attribute); + } + + if (is_void(node.name)) { + child_context.write(' />'); + } else { + child_context.write('>'); + + if (node.fragment) { + block(child_context, node.fragment, child_context.measure() < 30); + child_context.write(``); + } + } + + context.append(child_context); + }, + + RelativeSelector(node, context) { + if (node.combinator) { + if (node.combinator.name === ' ') { + context.write(' '); + } else { + context.write(` ${node.combinator.name} `); + } + } + + for (const selector of node.selectors) { + context.visit(selector); + } + }, + + RenderTag(node, context) { + context.write('{@render '); + context.visit(node.expression); + context.write('}'); + }, + + Rule(node, context) { + let started = false; + + for (const selector of node.prelude.children) { + if (started) { + context.write(','); + context.newline(); + } + + context.visit(selector); + started = true; + } + + context.write(' '); + context.visit(node.block); + }, + + SelectorList(node, context) { + let started = false; + for (const selector of node.children) { + if (started) { + context.write(', '); + } + + context.visit(selector); + started = true; + } + }, + + SlotElement(node, context) { + context.write(' 0) { + context.write('>'); + context.visit(node.fragment); // TODO block/inline + context.write(''); + } else { + context.write(' />'); + } + }, + + SnippetBlock(node, context) { + context.write('{#snippet '); + context.visit(node.expression); + + if (node.typeParams) { + context.write(`<${node.typeParams}>`); + } + + context.write('('); + + for (let i = 0; i < node.parameters.length; i += 1) { + if (i > 0) context.write(', '); + context.visit(node.parameters[i]); + } + + context.write(')}'); + block(context, node.body); + context.write('{/snippet}'); + }, + + SpreadAttribute(node, context) { + context.write('{...'); + context.visit(node.expression); + context.write('}'); + }, + + StyleDirective(node, context) { + context.write(`style:${node.name}`); + for (const modifier of node.modifiers) { + context.write(`|${modifier}`); + } + + if (node.value === true) { + return; + } + + context.write('='); + + if (Array.isArray(node.value)) { + context.write('"'); + + for (const tag of node.value) { + context.visit(tag); + } + + context.write('"'); + } else { + context.visit(node.value); + } + }, + + StyleSheet(node, context) { + context.write(''); + + if (node.children.length > 0) { + context.indent(); + context.newline(); + + let started = false; + + for (const child of node.children) { + if (started) { + context.margin(); + context.newline(); + } + + context.visit(child); + started = true; + } + + context.dedent(); + context.newline(); + } + + context.write(''); + }, + + SvelteBoundary(node, context) { + context.write(''); + block(context, node.fragment, true); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteComponent(node, context) { + context.write(''); + block(context, node.fragment, true); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteDocument(node, context) { + context.write(''); + block(context, node.fragment, true); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteElement(node, context) { + context.write(''); + block(context, node.fragment, true); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteFragment(node, context) { + context.write(''); + block(context, node.fragment, true); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteHead(node, context) { + context.write(''); + block(context, node.fragment, true); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteSelf(node, context) { + context.write(''); + block(context, node.fragment, true); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteWindow(node, context) { + context.write(''); + block(context, node.fragment, true); + context.write(``); + } else { + context.write('/>'); + } + }, + + Text(node, context) { + context.write(node.data); + }, + + TransitionDirective(node, context) { + const directive = node.intro && node.outro ? 'transition' : node.intro ? 'in' : 'out'; + context.write(`${directive}:${node.name}`); + for (const modifier of node.modifiers) { + context.write(`|${modifier}`); + } + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } + }, + + TypeSelector(node, context) { + context.write(node.name); + }, + + UseDirective(node, context) { + context.write(`use:${node.name}`); + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } + } +}; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 2a7ec7b5c6dc..c9841e9d4ee6 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 ` + +

+ Hello {name}! +

diff --git a/packages/svelte/tests/print/samples/basic/output.svelte b/packages/svelte/tests/print/samples/basic/output.svelte new file mode 100644 index 000000000000..22b3c84db099 --- /dev/null +++ b/packages/svelte/tests/print/samples/basic/output.svelte @@ -0,0 +1,5 @@ + + +

Hello {name}!

\ No newline at end of file diff --git a/packages/svelte/tests/print/samples/block/input.svelte b/packages/svelte/tests/print/samples/block/input.svelte new file mode 100644 index 000000000000..470f7a1efbdc --- /dev/null +++ b/packages/svelte/tests/print/samples/block/input.svelte @@ -0,0 +1 @@ +{#if condition} yes {:else} no {/if} diff --git a/packages/svelte/tests/print/samples/block/output.svelte b/packages/svelte/tests/print/samples/block/output.svelte new file mode 100644 index 000000000000..e0ff317fc8f4 --- /dev/null +++ b/packages/svelte/tests/print/samples/block/output.svelte @@ -0,0 +1,5 @@ +{#if condition} + yes +{:else} + no +{/if} diff --git a/packages/svelte/tests/print/test.ts b/packages/svelte/tests/print/test.ts new file mode 100644 index 000000000000..c678dedf3f36 --- /dev/null +++ b/packages/svelte/tests/print/test.ts @@ -0,0 +1,32 @@ +import * as fs from 'node:fs'; +import { assert, it } from 'vitest'; +import { parse, print } from 'svelte/compiler'; +import { try_load_json } from '../helpers.js'; +import { suite, type BaseTest } from '../suite.js'; +import { walk } from 'zimmerframe'; +import type { AST } from 'svelte/compiler'; + +interface ParserTest extends BaseTest {} + +const { test, run } = suite(async (config, cwd) => { + const input = fs.readFileSync(`${cwd}/input.svelte`, 'utf-8'); + + const ast = parse(input, { modern: true }); + const output = print(ast); + + // run `UPDATE_SNAPSHOTS=true pnpm test parser` to update parser tests + if (process.env.UPDATE_SNAPSHOTS) { + fs.writeFileSync(`${cwd}/output.svelte`, output.code); + } else { + fs.writeFileSync(`${cwd}/_actual.svelte`, output.code); + + const file = `${cwd}/output.svelte`; + + const expected = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : ''; + assert.deepEqual(output.code.trim().replaceAll('\r', ''), expected.trim().replaceAll('\r', '')); + } +}); + +export { test }; + +await run(__dirname); 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 ba3f4b155a31..a87a356d580b 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 cadae2cf15c0..e2c0ee29a587 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 28bb01fb18df..d84b674f88f4 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 4ea5edb6a0ac..cf731d8187b4 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 762a23754c9b..218951b83610 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 88f6f55ee74a..7d37abd97b1c 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 792d5421e1be..d4034dc55dd7 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 ebbe191dcbe4..0eab38919c5e 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 4cd6bc59d782..2ed863d68f3a 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 1a83e0d0f100..b295dbdac6cd 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1120,6 +1120,8 @@ declare module 'svelte/compiler' { instance: Script | null; /** The parsed `