From a0f86c5e1b77f4768793ac918edc87631486c7db Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 15 Jun 2025 15:41:51 -0400 Subject: [PATCH 01/37] use local version of esrap, for now --- package.json | 8 ++++++++ packages/svelte/package.json | 4 ++-- .../_expected/client/index.svelte.js | 1 + .../_expected/server/index.svelte.js | 1 + .../_expected/client/main.svelte.js | 1 + .../_expected/server/main.svelte.js | 1 + .../_expected/client/index.svelte.js | 2 ++ .../_expected/server/index.svelte.js | 2 ++ .../_expected/client/index.svelte.js | 1 + .../_expected/client/index.svelte.js | 1 - .../_expected/client/module.svelte.js | 1 + .../_expected/server/index.svelte.js | 1 - .../_expected/server/module.svelte.js | 1 + pnpm-lock.yaml | 18 +++++++++--------- 14 files changed, 30 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 62581782d72d..4f5748cc5dd2 100644 --- a/package.json +++ b/package.json @@ -42,5 +42,13 @@ "typescript-eslint": "^8.24.0", "v8-natives": "^1.2.5", "vitest": "^2.1.9" + }, + "pnpm": { + "overrides": { + "esrap": "link:../../esrap" + } + }, + "dependencies": { + "esrap": "link:../../../../esrap" } } diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d2fbdb32f74c..1b1276182b55 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": "https://pkg.pr.new/sveltejs/esrap@a275a5c", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", 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..884e919f14d8 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 @@ -4,5 +4,4 @@ import * as $ from 'svelte/internal/client'; import { random } from './module.svelte'; 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/client/module.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/module.svelte.js index 0d366e6258ff..feab7bf8dad8 100644 --- a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/module.svelte.js +++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/module.svelte.js @@ -1,5 +1,6 @@ /* module.svelte.js generated by Svelte VERSION */ import * as $ from 'svelte/internal/client'; + import { random } from './export'; export { random }; \ 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..75de235220bd 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 @@ -2,5 +2,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 diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/module.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/module.svelte.js index 2e0af8af84d8..fbbf1b955e99 100644 --- a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/module.svelte.js +++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/module.svelte.js @@ -1,5 +1,6 @@ /* module.svelte.js generated by Svelte VERSION */ import * as $ from 'svelte/internal/server'; + import { random } from './export'; export { random }; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfbc54df3363..08373e6b9d36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,9 +4,16 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + esrap: link:../../esrap + importers: .: + dependencies: + esrap: + specifier: link:../../esrap + version: link:../../esrap devDependencies: '@changesets/cli': specifier: ^2.27.8 @@ -87,8 +94,8 @@ importers: specifier: ^1.2.1 version: 1.2.1 esrap: - specifier: ^1.4.8 - version: 1.4.8 + specifier: link:../../../../esrap + version: link:../../../../esrap is-reference: specifier: ^3.0.3 version: 3.0.3 @@ -1261,9 +1268,6 @@ packages: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} - esrap@1.4.8: - resolution: {integrity: sha512-jlENbjZ7lqgJV9/OmgAtVqrFFMwsl70ctOgPIg5oTdQVGC13RSkMdtvAmu7ZTLax92c9ljnIG0xleEkdL69hwg==} - esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -3622,10 +3626,6 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@1.4.8: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - esrecurse@4.3.0: dependencies: estraverse: 5.3.0 From 486d10c8806482940d15435a3c87d66a37a89525 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 16 Jun 2025 14:00:14 -0400 Subject: [PATCH 02/37] WIP --- .../svelte/scripts/process-messages/index.js | 133 +++++++----------- packages/svelte/src/compiler/errors.js | 18 ++- packages/svelte/src/compiler/index.js | 1 + packages/svelte/src/compiler/legacy.js | 1 + .../src/compiler/phases/3-transform/index.js | 7 +- packages/svelte/src/compiler/print/index.js | 133 ++++++++++++++++++ .../svelte/src/compiler/types/template.d.ts | 2 +- packages/svelte/src/compiler/warnings.js | 29 ++-- packages/svelte/src/internal/client/errors.js | 24 +++- .../svelte/src/internal/client/warnings.js | 24 +++- packages/svelte/src/internal/server/errors.js | 3 + packages/svelte/src/internal/shared/errors.js | 6 + .../svelte/src/internal/shared/warnings.js | 10 +- packages/svelte/tests/parser-modern/test.ts | 25 +++- packages/svelte/types/index.d.ts | 6 +- playgrounds/sandbox/run.js | 6 +- 16 files changed, 312 insertions(+), 116 deletions(-) create mode 100644 packages/svelte/src/compiler/print/index.js diff --git a/packages/svelte/scripts/process-messages/index.js b/packages/svelte/scripts/process-messages/index.js index 81c59271de2e..d246cfbba5c4 100644 --- a/packages/svelte/scripts/process-messages/index.js +++ b/packages/svelte/scripts/process-messages/index.js @@ -4,6 +4,7 @@ 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'; @@ -98,55 +99,18 @@ function run() { .replace(/\r\n/g, '\n'); /** - * @type {Array<{ - * type: string; - * value: string; - * start: number; - * end: number - * }>} + * @type {any[]} */ 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'), ''); - } - - comments.push({ type: block ? 'Block' : 'Line', value, start, end }); - } + locations: true, + onComment: comments }); 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') { @@ -161,11 +125,6 @@ function run() { } }); - if (comments.length > 0) { - // @ts-expect-error - (ast.trailingComments ||= []).push(...comments); - } - const category = messages[name]; // find the `export function CODE` node @@ -184,6 +143,16 @@ function run() { const template_node = ast.body[index]; ast.body.splice(index, 1); + const jsdoc = comments.findLast((comment) => comment.start < template_node.start); + + const printed = esrap.print( + ast, + // @ts-expect-error + ts({ + comments: comments.filter((comment) => comment !== jsdoc) + }) + ); + for (const code in category) { const { messages } = category[code]; /** @type {string[]} */ @@ -273,41 +242,6 @@ function run() { } 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'); - } - - if (line.includes('PARAMETER')) { - return vars - .map((name, i) => { - const optional = i >= group[0].vars.length; - - return optional - ? ` * @param {string | undefined | null} [${name}]` - : ` * @param {string} ${name}`; - }) - .join('\n'); - } - - return line; - }) - .filter((x) => x !== '') - .join('\n'); - - if (value !== node.value) { - return { ...node, value }; - } - }, FunctionDeclaration(node, context) { if (node.id.name !== 'CODE') return; @@ -394,16 +328,49 @@ function run() { } }); + 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 (line.includes('PARAMETER')) { + return vars + .map((name, i) => { + const optional = i >= group[0].vars.length; + + return optional + ? ` * @param {string | undefined | null} [${name}]` + : ` * @param {string} ${name}`; + }) + .join('\n'); + } + + return line; + }) + .filter((x) => x !== '') + .join('\n') + }; + + // @ts-expect-error + const block = esrap.print({ ...ast, body: [clone] }, ts({ comments: [jsdoc_clone] })).code; + + printed.code += `\n\n${block}`; + // @ts-expect-error ast.body.push(clone); } - const module = esrap.print(ast); - 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..ec71fc85a275 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -4,21 +4,25 @@ import { CompileDiagnostic } from './utils/compile_diagnostic.js'; /** @typedef {{ start?: number, end?: number }} NodeLike */ class InternalCompileError extends Error { - message = ''; // ensure this property is enumerable + message = ''; + + // ensure this property is enumerable #diagnostic; /** - * @param {string} code - * @param {string} message - * @param {[number, number] | undefined} position - */ + * @param {string} code + * @param {string} message + * @param {[number, number] | undefined} position + */ 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 +820,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..ac0797927eb6 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 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/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index f96fd64ec7a9..2d045bf36362 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -1,6 +1,7 @@ /** @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 +35,8 @@ export function transform_component(analysis, source, options) { const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte'); - const js = print(program, { + // @ts-ignore TODO + const js = print(program, ts(), { // 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, @@ -94,7 +96,8 @@ export function transform_module(analysis, source, options) { } return { - js: print(program, { + // @ts-expect-error + js: print(program, ts(), { // 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, diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js new file mode 100644 index 000000000000..d3c6f31c77ad --- /dev/null +++ b/packages/svelte/src/compiler/print/index.js @@ -0,0 +1,133 @@ +/** @import { AST } from '#compiler'; */ +/** @import { Visitors } from 'esrap' */ +import * as esrap from 'esrap'; +import ts from 'esrap/languages/ts'; + +/** + * @param {AST.SvelteNode} ast + */ +export function print(ast) { + // @ts-expect-error some bullshit + return esrap.print(ast, { + ...ts(), + ...visitors + }); +} + +/** @type {Visitors} */ +const visitors = { + Root(node, context) { + if (node.options) { + throw new Error('TODO'); + } + + for (const item of [node.module, node.instance, node.fragment, node.css]) { + if (!item) continue; + + context.margin(); + context.newline(); + context.visit(item); + } + }, + Script(node, context) { + context.write(''); + + context.indent(); + context.newline(); + context.visit(node.content); + context.dedent(); + context.newline(); + + context.write(''); + }, + Fragment(node, context) { + for (let i = 0; i < node.nodes.length; i += 1) { + const child = node.nodes[i]; + + if (child.type === 'Text') { + let data = child.data; + + if (i === 0) data = data.trimStart(); + if (i === node.nodes.length - 1) data = data.trimEnd(); + + context.write(data); + } else { + context.visit(child); + } + } + }, + Attribute(node, context) { + context.write(node.name); + + if (node.value === true) return; + + context.write('='); + + if (Array.isArray(node.value)) { + if (node.value.length > 1) { + context.write('"'); + } + + for (const chunk of node.value) { + context.visit(chunk); + } + + if (node.value.length > 1) { + context.write('"'); + } + } else { + context.visit(node.value); + } + }, + Text(node, context) { + context.write(node.data); + }, + ExpressionTag(node, context) { + context.write('{'); + context.visit(node.expression); + context.write('}'); + }, + IfBlock(node, context) { + context.write('{#if '); + context.visit(node.test); + context.write('}'); + + context.visit(node.consequent); + + // TODO handle alternate/else if + + context.write('{/if}'); + }, + RegularElement(node, context) { + context.write('<' + node.name); + + for (const attribute of node.attributes) { + // TODO handle multiline + context.write(' '); + context.visit(attribute); + } + + context.write('>'); + + // TODO handle void elements + if (node.fragment) { + context.visit(node.fragment); + } + + context.write(``); + }, + TransitionDirective(node, context) { + // TODO + } +}; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index cefc7fa7a20d..7d2a0397b185 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -572,7 +572,7 @@ export namespace AST { | AST.Comment | Block; - export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node; + export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node | Script; export type { _CSS as CSS }; } diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 1226190891b2..e6bc7adf9cee 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -1,12 +1,6 @@ /* This file is generated by scripts/process-messages/index.js. Do not edit! */ -import { - warnings, - ignore_stack, - ignore_map, - warning_filter -} from './state.js'; - +import { warnings, ignore_stack, ignore_map, warning_filter } from './state.js'; import { CompileDiagnostic } from './utils/compile_diagnostic.js'; /** @typedef {{ start?: number, end?: number }} NodeLike */ @@ -14,10 +8,10 @@ class InternalCompileWarning extends CompileDiagnostic { name = 'CompileWarning'; /** - * @param {string} code - * @param {string} message - * @param {[number, number] | undefined} position - */ + * @param {string} code + * @param {string} message + * @param {[number, number] | undefined} position + */ constructor(code, message, position) { super(code, message, position); } @@ -40,6 +34,7 @@ function w(node, code, message) { const warning = new InternalCompileWarning(code, message, node && node.start !== undefined ? [node.start, node.end ?? node.start] : undefined); if (!warning_filter(warning)) return; + warnings.push(warning); } @@ -496,7 +491,9 @@ export function a11y_role_supports_aria_props_implicit(node, attribute, role, na * @param {string | undefined | null} [suggestion] */ export function a11y_unknown_aria_attribute(node, attribute, suggestion) { - w(node, 'a11y_unknown_aria_attribute', `${suggestion ? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?` : `Unknown aria attribute 'aria-${attribute}'`}\nhttps://svelte.dev/e/a11y_unknown_aria_attribute`); + w(node, 'a11y_unknown_aria_attribute', `${suggestion + ? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?` + : `Unknown aria attribute 'aria-${attribute}'`}\nhttps://svelte.dev/e/a11y_unknown_aria_attribute`); } /** @@ -506,7 +503,9 @@ export function a11y_unknown_aria_attribute(node, attribute, suggestion) { * @param {string | undefined | null} [suggestion] */ export function a11y_unknown_role(node, role, suggestion) { - w(node, 'a11y_unknown_role', `${suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`); + w(node, 'a11y_unknown_role', `${suggestion + ? `Unknown role '${role}'. Did you mean '${suggestion}'?` + : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`); } /** @@ -534,7 +533,9 @@ export function legacy_code(node, code, suggestion) { * @param {string | undefined | null} [suggestion] */ export function unknown_code(node, code, suggestion) { - w(node, 'unknown_code', `${suggestion ? `\`${code}\` is not a recognised code (did you mean \`${suggestion}\`?)` : `\`${code}\` is not a recognised code`}\nhttps://svelte.dev/e/unknown_code`); + w(node, 'unknown_code', `${suggestion + ? `\`${code}\` is not a recognised code (did you mean \`${suggestion}\`?)` + : `\`${code}\` is not a recognised code`}\nhttps://svelte.dev/e/unknown_code`); } /** diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 429dd99da9b9..042cd9132e7f 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,6 +188,7 @@ 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`); @@ -191,6 +204,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 +220,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 +237,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 +254,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 +271,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 +288,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 +304,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 +320,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 +336,7 @@ export function state_unsafe_mutation() { const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived 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/warnings.js b/packages/svelte/src/internal/client/warnings.js index e07892a4b064..74f9041b91dd 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/errors.js b/packages/svelte/src/internal/server/errors.js index 38c545c84ec8..e47530c9aaf9 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/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index b8606fbf6f7d..6bcc35016a70 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 281be0838211..0acca4418410 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 b47d4a48796e..08d8aafeab0c 100644 --- a/packages/svelte/tests/parser-modern/test.ts +++ b/packages/svelte/tests/parser-modern/test.ts @@ -1,8 +1,9 @@ import * as fs from 'node:fs'; import { assert, it } from 'vitest'; -import { parse } from 'svelte/compiler'; +import { parse, print } from 'svelte/compiler'; import { try_load_json } from '../helpers.js'; import { suite, type BaseTest } from '../suite.js'; +import { walk } from 'zimmerframe'; interface ParserTest extends BaseTest {} @@ -30,6 +31,28 @@ const { test, run } = suite(async (config, cwd) => { const expected = try_load_json(`${cwd}/output.json`); assert.deepEqual(actual, expected); } + + const printed = print(actual); + const reparsed = JSON.parse( + JSON.stringify( + parse(printed.code, { + modern: true, + loose: cwd.split('/').pop()!.startsWith('loose-') + }) + ) + ); + + fs.writeFileSync(`${cwd}/_actual.svelte`, JSON.stringify(printed.code, null, '\t')); + + const actual_cleaned = walk(actual, null, { + _(node, context) {} + }); + + const reparsed_cleaned = walk(actual, null, { + _(node, context) {} + }); + + assert.deepEqual(actual_cleaned, reparsed_cleaned); }); export { test }; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 1a83e0d0f100..b8dbc96a115d 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1493,7 +1493,7 @@ declare module 'svelte/compiler' { | AST.Comment | Block; - export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node; + export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node | Script; export type { _CSS as CSS }; } @@ -1505,6 +1505,10 @@ declare module 'svelte/compiler' { export function preprocess(source: string, preprocessor: PreprocessorGroup | PreprocessorGroup[], options?: { filename?: string; } | undefined): Promise; + export function print(ast: AST.SvelteNode): { + code: string; + map: any; + }; /** * The current version, as set in package.json. * */ diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 2029937f52dc..348e43b9c062 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -3,7 +3,7 @@ import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; import { globSync } from 'tinyglobby'; -import { compile, compileModule, parse, migrate } from 'svelte/compiler'; +import { compile, compileModule, parse, print, migrate } from 'svelte/compiler'; const argv = parseArgs({ options: { runes: { type: 'boolean' } }, args: process.argv.slice(2) }); @@ -70,6 +70,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { } catch (e) { console.warn(`Error migrating ${file}`, e); } + + const printed = print(ast); + + write(`${cwd}/output/printed/${file}`, printed.code); } const compiled = compile(source, { From 97c5d98f7944666ecf9de57e9088ba288ca1e67a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 16 Jun 2025 16:59:26 -0400 Subject: [PATCH 03/37] WIP --- packages/svelte/src/compiler/index.js | 2 +- .../src/compiler/phases/1-parse/acorn.js | 51 ++++++++++++------- .../src/compiler/phases/1-parse/index.js | 2 + .../compiler/phases/1-parse/read/context.js | 11 ++-- .../phases/1-parse/read/expression.js | 7 ++- .../compiler/phases/1-parse/read/script.js | 2 +- .../src/compiler/phases/1-parse/state/tag.js | 7 ++- .../src/compiler/phases/2-analyze/index.js | 14 +++-- .../3-transform/client/transform-client.js | 3 ++ .../src/compiler/phases/3-transform/index.js | 22 +++++--- .../3-transform/server/transform-server.js | 3 ++ .../svelte/src/compiler/phases/types.d.ts | 2 + packages/svelte/src/compiler/print/index.js | 9 +++- .../svelte/src/compiler/types/template.d.ts | 2 + .../_expected/client/module.svelte.js | 3 +- .../_expected/server/module.svelte.js | 3 +- 16 files changed, 103 insertions(+), 40 deletions(-) diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index ac0797927eb6..11db09193607 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -70,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/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 26a09abb66b7..5a1e693bdccb 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -5,14 +5,27 @@ import { tsPlugin } from '@sveltejs/acorn-typescript'; const ParserWithTS = acorn.Parser.extend(tsPlugin()); +/** + * @typedef {Comment & { + * start: number; + * end: number; + * }} CommentWithLocation + */ + /** * @param {string} source + * @param {Comment[]} 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 +66,18 @@ 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) + ); const ast = parser.parseExpressionAt(source, index, { onComment, @@ -78,18 +96,9 @@ 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 */ -function get_comment_handlers(source) { - /** - * @typedef {Comment & { - * start: number; - * end: number; - * }} CommentWithLocation - */ - - /** @type {CommentWithLocation[]} */ - const comments = []; - +function get_comment_handlers(source, comments) { return { /** * @param {boolean} block @@ -97,7 +106,7 @@ function get_comment_handlers(source) { * @param {number} start * @param {number} end */ - 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 +118,21 @@ 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: start_loc, end: end_loc } + }); }, /** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */ add_comments(ast) { if (comments.length === 0) return; + comments = comments.slice(); + 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..bad0c4ae9610 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js @@ -34,7 +34,12 @@ 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); + const node = parse_expression_at( + parser.template, + parser.root.comments, + parser.ts, + parser.index + ); let num_parens = 0; 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 4153463c8361..f86b7bfec64f 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -389,7 +389,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 fded183b86c3..530089dd67a7 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 {Comment[]} */ + 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, tracing: false, 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 2d045bf36362..ae49470d7c68 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -36,7 +36,7 @@ export function transform_component(analysis, source, options) { const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte'); // @ts-ignore TODO - const js = print(program, ts(), { + const js = print(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, @@ -95,14 +95,20 @@ export function transform_module(analysis, source, options) { ]; } + // @ts-expect-error + const js = print(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 { - // @ts-expect-error - js: print(program, ts(), { - // 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 67cbd75ff86f..aeb6184724a9 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: Comment[]; classes: Map>; diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index d3c6f31c77ad..bf17b4c1e6f6 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -9,7 +9,8 @@ import ts from 'esrap/languages/ts'; export function print(ast) { // @ts-expect-error some bullshit return esrap.print(ast, { - ...ts(), + // @ts-expect-error some bullshit + ...ts({ comments: ast.type === 'Root' ? ast.comments : [] }), ...visitors }); } @@ -127,7 +128,13 @@ const visitors = { context.write(``); }, + OnDirective(node, context) { + // TODO + }, TransitionDirective(node, context) { // TODO + }, + Comment(node, context) { + // TODO } }; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 7d2a0397b185..60f6ec3bdbac 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 `'); }, + Fragment(node, context) { for (let i = 0; i < node.nodes.length; i += 1) { const child = node.nodes[i]; @@ -68,6 +80,25 @@ const visitors = { } } }, + + 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); @@ -91,14 +122,159 @@ const visitors = { context.visit(node.value); } }, - Text(node, context) { - context.write(node.data); + + AwaitBlock(node, context) { + context.write(`{#await `); + context.visit(node.expression); + + if (node.pending) { + context.write('}'); + context.visit(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('}'); + context.visit(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('}'); + context.visit(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('}'); + }, + + 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('>'); + context.visit(node.fragment); + context.write(``); + } else { + context.write(' />'); + } + }, + + Declaration(node, context) { + context.write('foo: bar;'); + }, + + 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('}'); + context.visit(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('}'); }, + IfBlock(node, context) { context.write('{#if '); context.visit(node.test); @@ -110,6 +286,41 @@ const visitors = { context.write('{/if}'); }, + + Nth(node, context) { + context.write(node.value); // TODO is this right? + }, + + OnDirective(node, context) { + // TODO + }, + + 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) { context.write('<' + node.name); @@ -119,22 +330,134 @@ const visitors = { context.visit(attribute); } - context.write('>'); + if (is_void(node.name)) { + context.write(' />'); + } else { + context.write('>'); + + if (node.fragment) { + context.visit(node.fragment); + context.write(``); + } + } + }, + + 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); + }, + + SlotElement(node, context) { + context.write(' 0) { + context.write('>'); context.visit(node.fragment); + context.write(''); + } else { + context.write(' />'); } + }, - 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(')}'); + context.visit(node.body); + context.write('{/snippet}'); }, - OnDirective(node, context) { - // TODO + + 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(''); + }, + + Text(node, context) { + context.write(node.data); }, + TransitionDirective(node, context) { // TODO }, - Comment(node, context) { - // TODO + + TypeSelector(node, context) { + context.write(node.name); } }; diff --git a/packages/svelte/tests/parser-modern/test.ts b/packages/svelte/tests/parser-modern/test.ts index 08d8aafeab0c..da9119441a04 100644 --- a/packages/svelte/tests/parser-modern/test.ts +++ b/packages/svelte/tests/parser-modern/test.ts @@ -8,6 +8,8 @@ import { walk } from 'zimmerframe'; interface ParserTest extends BaseTest {} const { test, run } = suite(async (config, cwd) => { + const loose = cwd.split('/').pop()!.startsWith('loose-'); + const input = fs .readFileSync(`${cwd}/input.svelte`, 'utf-8') .replace(/\s+$/, '') @@ -22,6 +24,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')); @@ -32,27 +36,37 @@ const { test, run } = suite(async (config, cwd) => { assert.deepEqual(actual, expected); } - const printed = print(actual); - const reparsed = JSON.parse( - JSON.stringify( - parse(printed.code, { - modern: true, - loose: cwd.split('/').pop()!.startsWith('loose-') - }) - ) - ); + if (!loose) { + const printed = print(actual); + const reparsed = JSON.parse( + JSON.stringify( + parse(printed.code, { + modern: true, + loose + }) + ) + ); - fs.writeFileSync(`${cwd}/_actual.svelte`, JSON.stringify(printed.code, null, '\t')); + fs.writeFileSync(`${cwd}/_actual.svelte`, printed.code); - const actual_cleaned = walk(actual, null, { - _(node, context) {} - }); + const actual_cleaned = walk(actual, null, { + _(node, context) { + delete node.loc; + context.next(); + } + }); - const reparsed_cleaned = walk(actual, null, { - _(node, context) {} - }); + delete reparsed.comments; - assert.deepEqual(actual_cleaned, reparsed_cleaned); + const reparsed_cleaned = walk(reparsed, null, { + _(node, context) { + delete node.loc; + context.next(); + } + }); + + assert.deepEqual(actual_cleaned, reparsed_cleaned); + } }); export { test }; From 683ac717adb97dfb21009f8d0c863a56919186d7 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:04:09 -0700 Subject: [PATCH 06/37] add `Declaration` visitor --- packages/svelte/src/compiler/print/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 7e77da73d8a8..72908d283832 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -1,4 +1,5 @@ /** @import { AST } from '#compiler'; */ +/** @import { _CSS } from '../types/css.js'; */ /** @import { Visitors } from 'esrap' */ import * as esrap from 'esrap'; import ts from 'esrap/languages/ts'; @@ -16,7 +17,7 @@ export function print(ast) { }); } -/** @type {Visitors} */ +/** @type {Visitors} */ const visitors = { Root(node, context) { if (node.options) { @@ -236,7 +237,7 @@ const visitors = { }, Declaration(node, context) { - context.write('foo: bar;'); + context.write(`${node.property}: ${node.value};`); }, EachBlock(node, context) { From 45755ea852a86c47b5639a6191381ec181a8e703 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:08:08 -0700 Subject: [PATCH 07/37] add `TransitionDirective` --- packages/svelte/src/compiler/print/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 72908d283832..7a4e2686fe95 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -455,7 +455,16 @@ const visitors = { }, TransitionDirective(node, context) { - // TODO + 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) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } }, TypeSelector(node, context) { From 222dd4102230a99b69bd84290b0b57a5368b1f09 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:12:43 -0700 Subject: [PATCH 08/37] `UseDirective`, `OnDirective` --- packages/svelte/src/compiler/print/index.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 7a4e2686fe95..8cbffd468066 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -293,7 +293,15 @@ const visitors = { }, OnDirective(node, context) { - // TODO + context.write(`on:${node.name}`); + for (const modifier of node.modifiers) { + context.write(`|${modifier}`); + } + if (node.expression !== null) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } }, PseudoClassSelector(node, context) { @@ -469,5 +477,14 @@ const visitors = { TypeSelector(node, context) { context.write(node.name); + }, + + UseDirective(node, context) { + context.write(`use:${node.name}`); + if (node.expression !== null) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } } }; From 1e7b439533f3d3fbbb62a933b4ba2b236374cb4b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:16:49 -0700 Subject: [PATCH 09/37] more directives --- packages/svelte/src/compiler/print/index.js | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 8cbffd468066..6faa247ce66d 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -205,6 +205,15 @@ const visitors = { context.write('}'); }, + ClassDirective(node, context) { + context.write(`class:${node.name}`); + if (node.expression !== null) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } + }, + ClassSelector(node, context) { context.write(`.${node.name}`); }, @@ -288,6 +297,15 @@ const visitors = { context.write('{/if}'); }, + LetDirective(node, context) { + context.write(`let:${node.name}`); + if (node.expression !== null) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } + }, + Nth(node, context) { context.write(node.value); // TODO is this right? }, @@ -425,6 +443,18 @@ const visitors = { context.write('{/snippet}'); }, + StyleDirective(node, context) { + context.write(`style:${node.name}`); + for (const modifier of node.modifiers) { + context.write(`|${modifier}`); + } + if (node.expression !== null) { + context.write('={'); + context.visit(node.expression); + context.write('}'); + } + }, + StyleSheet(node, context) { context.write(' Date: Mon, 16 Jun 2025 20:36:39 -0700 Subject: [PATCH 10/37] `SpreadAttribute`, directive shorthands --- packages/svelte/src/compiler/print/index.js | 39 ++++++++++++++++----- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 6faa247ce66d..aaa1e1566a81 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -1,5 +1,4 @@ /** @import { AST } from '#compiler'; */ -/** @import { _CSS } from '../types/css.js'; */ /** @import { Visitors } from 'esrap' */ import * as esrap from 'esrap'; import ts from 'esrap/languages/ts'; @@ -17,7 +16,7 @@ export function print(ast) { }); } -/** @type {Visitors} */ +/** @type {Visitors} */ const visitors = { Root(node, context) { if (node.options) { @@ -207,7 +206,10 @@ const visitors = { ClassDirective(node, context) { context.write(`class:${node.name}`); - if (node.expression !== null) { + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { context.write('={'); context.visit(node.expression); context.write('}'); @@ -299,7 +301,10 @@ const visitors = { LetDirective(node, context) { context.write(`let:${node.name}`); - if (node.expression !== null) { + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { context.write('={'); context.visit(node.expression); context.write('}'); @@ -315,7 +320,10 @@ const visitors = { for (const modifier of node.modifiers) { context.write(`|${modifier}`); } - if (node.expression !== null) { + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { context.write('={'); context.visit(node.expression); context.write('}'); @@ -443,12 +451,21 @@ const visitors = { 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.expression !== null) { + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { context.write('={'); context.visit(node.expression); context.write('}'); @@ -498,7 +515,10 @@ const visitors = { for (const modifier of node.modifiers) { context.write(`|${modifier}`); } - if (node.expression !== null) { + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { context.write('={'); context.visit(node.expression); context.write('}'); @@ -511,7 +531,10 @@ const visitors = { UseDirective(node, context) { context.write(`use:${node.name}`); - if (node.expression !== null) { + if ( + node.expression !== null && + !(node.expression.type === 'Identifier' && node.expression.name === node.name) + ) { context.write('={'); context.visit(node.expression); context.write('}'); From 4c404ac44d485811db37a73541962b3fe4296eff Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:41:46 -0700 Subject: [PATCH 11/37] `{#if ...} {:else ...}` --- packages/svelte/src/compiler/print/index.js | 26 ++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index aaa1e1566a81..3618a9c33774 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -288,15 +288,25 @@ const visitors = { }, IfBlock(node, context) { - context.write('{#if '); - context.visit(node.test); - context.write('}'); - - context.visit(node.consequent); - - // TODO handle alternate/else if + if (node.elseif) { + context.write('{:else if '); + context.visit(node.test); + context.write('}'); + context.visit(node.consequent); + } else { + context.write('{#if '); + context.visit(node.test); + context.write('}'); - context.write('{/if}'); + context.visit(node.consequent); + if (node.alternate !== null) { + if (!(node.alternate.type === 'IfBlock' && node.alternate.elseif)) { + context.write('{:else}'); + } + context.visit(node.alternate); + } + context.write('{/if}'); + } }, LetDirective(node, context) { From c646f9742bb44e9f291c6c4248c71aa0d58cf9f4 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:42:49 -0700 Subject: [PATCH 12/37] fix --- packages/svelte/src/compiler/print/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 3618a9c33774..eb40fa355650 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -300,7 +300,13 @@ const visitors = { context.visit(node.consequent); if (node.alternate !== null) { - if (!(node.alternate.type === 'IfBlock' && node.alternate.elseif)) { + if ( + !( + node.alternate.nodes.length === 1 && + node.alternate.nodes[0].type === 'IfBlock' && + node.alternate.nodes[0].elseif + ) + ) { context.write('{:else}'); } context.visit(node.alternate); From d96412b63f5342c99a05fff44d637bd5d4c3f6b7 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:44:13 -0700 Subject: [PATCH 13/37] more --- packages/svelte/src/compiler/print/index.js | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index eb40fa355650..7495721d7ee4 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -299,18 +299,20 @@ const visitors = { context.write('}'); context.visit(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}'); - } - context.visit(node.alternate); + } + if (node.alternate !== null) { + if ( + !( + node.alternate.nodes.length === 1 && + node.alternate.nodes[0].type === 'IfBlock' && + node.alternate.nodes[0].elseif + ) + ) { + context.write('{:else}'); } + context.visit(node.alternate); + } + if (!node.elseif) { context.write('{/if}'); } }, From 566da5b95f0d451215603cfd8b03239a1191bbe7 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:54:21 -0700 Subject: [PATCH 14/37] add tags, `AnimateDirective` --- packages/svelte/src/compiler/print/index.js | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 7495721d7ee4..771c997fdf83 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -81,6 +81,18 @@ const visitors = { } }, + 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}`); @@ -247,6 +259,25 @@ const visitors = { } }, + ConstTag(node, context) { + context.write('{@const '); + context.visit(node.declaration); // TODO does this work? + 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};`); }, @@ -287,6 +318,12 @@ const visitors = { context.write('}'); }, + HtmlTag(node, context) { + context.write('{@html '); + context.visit(node.expression); + context.write('}'); + }, + IfBlock(node, context) { if (node.elseif) { context.write('{:else if '); From 0b9f5607c61dd62bbc5f9acb9d850d67c105feb7 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 16 Jun 2025 21:00:40 -0700 Subject: [PATCH 15/37] `KeyBlock` --- packages/svelte/src/compiler/print/index.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 771c997fdf83..213fa3b1f881 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -16,7 +16,7 @@ export function print(ast) { }); } -/** @type {Visitors} */ +/** @type {Visitors} */ const visitors = { Root(node, context) { if (node.options) { @@ -354,6 +354,14 @@ const visitors = { } }, + KeyBlock(node, context) { + context.write('{#key '); + context.visit(node.expression); + context.write('}'); + context.visit(node.fragment); + context.write('{/key}'); + }, + LetDirective(node, context) { context.write(`let:${node.name}`); if ( From 0de2182acc4072610fa51d45efeee920603dfb4c Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 16 Jun 2025 21:14:04 -0700 Subject: [PATCH 16/37] `SelectorList`, `` --- packages/svelte/src/compiler/print/index.js | 166 +++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 213fa3b1f881..ab5180fe6e32 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -261,7 +261,7 @@ const visitors = { ConstTag(node, context) { context.write('{@const '); - context.visit(node.declaration); // TODO does this work? + context.visit(node.declaration); context.write('}'); }, @@ -477,6 +477,18 @@ const visitors = { 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(''); }, + SvelteBoundary(node, context) { + context.write(''); + context.visit(node.fragment); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteComponent(node, context) { + context.write(''); + context.visit(node.fragment); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteDocument(node, context) { + context.write(''); + context.visit(node.fragment); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteElement(node, context) { + context.write(''); + context.visit(node.fragment); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteFragment(node, context) { + context.write(''); + context.visit(node.fragment); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteHead(node, context) { + context.write(''); + context.visit(node.fragment); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteSelf(node, context) { + context.write(''); + context.visit(node.fragment); + context.write(``); + } else { + context.write('/>'); + } + }, + + SvelteWindow(node, context) { + context.write(''); + context.visit(node.fragment); + context.write(``); + } else { + context.write('/>'); + } + }, + Text(node, context) { context.write(node.data); }, From e41bbe7a1cb053f6868a7978c11cc85407658aae Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 21 Jun 2025 12:22:24 -0700 Subject: [PATCH 17/37] quote text in `Attribute` visitor --- packages/svelte/src/compiler/print/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index ab5180fe6e32..45feedff4cc5 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -119,7 +119,7 @@ const visitors = { context.write('='); if (Array.isArray(node.value)) { - if (node.value.length > 1) { + if (node.value.length > 1 || node.value[0].type === 'Text') { context.write('"'); } @@ -127,7 +127,7 @@ const visitors = { context.visit(chunk); } - if (node.value.length > 1) { + if (node.value.length > 1 || node.value[0].type === 'Text') { context.write('"'); } } else { From 0a61c80be3a73c1380b40df55e37e3d145aa6ba7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 06:50:10 -0400 Subject: [PATCH 18/37] tweak test logic to reduce false negatives --- packages/svelte/src/compiler/print/index.js | 2 +- packages/svelte/tests/parser-modern/test.ts | 55 +++++++++++++++------ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 45feedff4cc5..f0c540987082 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -16,7 +16,7 @@ export function print(ast) { }); } -/** @type {Visitors} */ +/** @type {Visitors} */ const visitors = { Root(node, context) { if (node.options) { diff --git a/packages/svelte/tests/parser-modern/test.ts b/packages/svelte/tests/parser-modern/test.ts index da9119441a04..74fabda63369 100644 --- a/packages/svelte/tests/parser-modern/test.ts +++ b/packages/svelte/tests/parser-modern/test.ts @@ -49,26 +49,51 @@ const { test, run } = suite(async (config, cwd) => { fs.writeFileSync(`${cwd}/_actual.svelte`, printed.code); - const actual_cleaned = walk(actual, null, { - _(node, context) { - delete node.loc; - context.next(); - } - }); - delete reparsed.comments; - const reparsed_cleaned = walk(reparsed, null, { - _(node, context) { - delete node.loc; - context.next(); - } - }); - - assert.deepEqual(actual_cleaned, reparsed_cleaned); + assert.deepEqual(clean(actual), clean(reparsed)); } }); +function clean(ast: import('svelte/compiler').AST.SvelteNode) { + return walk(ast, null, { + _(node, context) { + // @ts-ignore + delete node.start; + // @ts-ignore + delete node.end; + // @ts-ignore + delete node.loc; + // @ts-ignore + delete node.leadingComments; + // @ts-ignore + delete node.trailingComments; + + context.next(); + }, + Fragment(node, context) { + return { + ...node, + nodes: node.nodes + .map((child, i) => { + if (child.type === 'Text') { + if (i === 0) { + child = { ...child, data: child.data.trimStart() }; + } + + if (i === node.nodes.length - 1) { + child = { ...child, data: child.data.trimEnd() }; + } + + if (!child.data) return null; + } + }) + .filter(Boolean) + }; + } + }); +} + export { test }; await run(__dirname); From 5db4ea878afb7a71e543e7bc32c20eec411e31b2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 06:52:56 -0400 Subject: [PATCH 19/37] fix --- packages/svelte/tests/parser-modern/test.ts | 42 +++++++++++---------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/svelte/tests/parser-modern/test.ts b/packages/svelte/tests/parser-modern/test.ts index 74fabda63369..152f8332a6ea 100644 --- a/packages/svelte/tests/parser-modern/test.ts +++ b/packages/svelte/tests/parser-modern/test.ts @@ -4,6 +4,7 @@ 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 {} @@ -55,7 +56,7 @@ const { test, run } = suite(async (config, cwd) => { } }); -function clean(ast: import('svelte/compiler').AST.SvelteNode) { +function clean(ast: AST.SvelteNode) { return walk(ast, null, { _(node, context) { // @ts-ignore @@ -72,24 +73,27 @@ function clean(ast: import('svelte/compiler').AST.SvelteNode) { context.next(); }, Fragment(node, context) { - return { - ...node, - nodes: node.nodes - .map((child, i) => { - if (child.type === 'Text') { - if (i === 0) { - child = { ...child, data: child.data.trimStart() }; - } - - if (i === node.nodes.length - 1) { - child = { ...child, data: child.data.trimEnd() }; - } - - if (!child.data) return null; - } - }) - .filter(Boolean) - }; + const nodes: AST.SvelteNode[] = []; + + for (let i = 0; i < node.nodes.length; i += 1) { + let child = node.nodes[i]; + + if (child.type === 'Text') { + if (i === 0) { + child = { ...child, data: child.data.trimStart() }; + } + + if (i === node.nodes.length - 1) { + child = { ...child, data: child.data.trimEnd() }; + } + + if (!child.data) continue; + } + + nodes.push(context.visit(child)); + } + + return { ...node, nodes } as AST.Fragment; } }); } From b80eb3e297af7babd914cb253d2801e389cb018d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 07:00:30 -0400 Subject: [PATCH 20/37] fix --- packages/svelte/tests/parser-modern/test.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/svelte/tests/parser-modern/test.ts b/packages/svelte/tests/parser-modern/test.ts index 152f8332a6ea..4130bd8d613e 100644 --- a/packages/svelte/tests/parser-modern/test.ts +++ b/packages/svelte/tests/parser-modern/test.ts @@ -72,6 +72,14 @@ function clean(ast: AST.SvelteNode) { context.next(); }, + StyleSheet(node, context) { + return { + type: node.type, + attributes: node.attributes.map((attribute) => context.visit(attribute)), + children: node.children.map((child) => context.visit(child)), + content: {} + } as AST.SvelteNode; + }, Fragment(node, context) { const nodes: AST.SvelteNode[] = []; @@ -80,11 +88,19 @@ function clean(ast: AST.SvelteNode) { if (child.type === 'Text') { if (i === 0) { - child = { ...child, data: child.data.trimStart() }; + child = { + ...child, + data: child.data.trimStart(), + raw: child.raw.trimStart() + }; } if (i === node.nodes.length - 1) { - child = { ...child, data: child.data.trimEnd() }; + child = { + ...child, + data: child.data.trimEnd(), + raw: child.raw.trimEnd() + }; } if (!child.data) continue; From 4018b5024724a9035ca934aa5a97d4f8a54e681a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 07:07:20 -0400 Subject: [PATCH 21/37] add separate test suite --- .../tests/print/samples/basic/input.svelte | 7 ++++ .../tests/print/samples/basic/output.svelte | 5 +++ packages/svelte/tests/print/test.ts | 32 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 packages/svelte/tests/print/samples/basic/input.svelte create mode 100644 packages/svelte/tests/print/samples/basic/output.svelte create mode 100644 packages/svelte/tests/print/test.ts diff --git a/packages/svelte/tests/print/samples/basic/input.svelte b/packages/svelte/tests/print/samples/basic/input.svelte new file mode 100644 index 000000000000..bff6bee537b4 --- /dev/null +++ b/packages/svelte/tests/print/samples/basic/input.svelte @@ -0,0 +1,7 @@ + + +

+ 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/test.ts b/packages/svelte/tests/print/test.ts new file mode 100644 index 000000000000..776a6a4ca47f --- /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, expected); + } +}); + +export { test }; + +await run(__dirname); From 1ca1be5a030010404f3bb60a20e858fafbba732c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 07:45:30 -0400 Subject: [PATCH 22/37] fix --- .../imports-in-modules/_expected/client/index.svelte.js | 3 +-- .../imports-in-modules/_expected/server/index.svelte.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) 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 884e919f14d8..ad6beb0c7b0c 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,5 +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) {} 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 75de235220bd..da97f06aada3 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,5 +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) {} From 80370e3a26e378c896272c116c94688105482fa2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 13:07:33 -0400 Subject: [PATCH 23/37] more --- packages/svelte/src/compiler/print/index.js | 85 +++++++++++++++++-- .../tests/print/samples/block/input.svelte | 1 + .../tests/print/samples/block/output.svelte | 5 ++ packages/svelte/tests/print/test.ts | 2 +- 4 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 packages/svelte/tests/print/samples/block/input.svelte create mode 100644 packages/svelte/tests/print/samples/block/output.svelte diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index f0c540987082..a387a31cace3 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -1,5 +1,5 @@ /** @import { AST } from '#compiler'; */ -/** @import { Visitors } from 'esrap' */ +/** @import { Context, Visitors } from 'esrap' */ import * as esrap from 'esrap'; import ts from 'esrap/languages/ts'; import { is_void } from '../../utils.js'; @@ -65,20 +65,71 @@ const visitors = { }, Fragment(node, context) { - for (let i = 0; i < node.nodes.length; i += 1) { - const child = node.nodes[i]; + const join = context.new(); + + /** @type {Context[]} */ + const contexts = []; - if (child.type === 'Text') { - let data = child.data; + let sequence = context.new(); - if (i === 0) data = data.trimStart(); - if (i === node.nodes.length - 1) data = data.trimEnd(); + let multiline = false; - context.write(data); + function flush() { + if (sequence.empty()) { + return; + } + + contexts.push(sequence); + sequence = context.new(); + } + + for (let i = 0; i < node.nodes.length; i += 1) { + const child_node = node.nodes[i]; + const prev = node.nodes[i - 1]; + const next = node.nodes[i + 1]; + + const prev_is_text = prev && (prev.type === 'Text' || prev.type === 'ExpressionTag'); + const next_is_text = next && (next.type === 'Text' || next.type === 'ExpressionTag'); + + if (child_node.type === 'Text' || child_node.type === 'ExpressionTag') { + if (child_node.type === 'Text') { + let { data } = child_node; + + let a = !prev_is_text && data !== (data = data.trimStart()); + let b = !next_is_text && data !== (data = data.trimEnd()); + + if (data === '') { + if (prev && next) sequence.append(join); + } else { + if (a && prev) sequence.append(join); + sequence.write(data); + if (b && next) sequence.append(join); + } + } else { + sequence.visit(child_node); + } } else { - context.visit(child); + flush(); + const child_context = context.new(); + child_context.visit(child_node); + + contexts.push(child_context); + + multiline ||= child_context.multiline; } } + + flush(); + + if (multiline) { + join.newline(); + } else { + join.write(' '); + } + + for (const child_context of contexts) { + context.append(child_context); + } }, AnimateDirective(node, context) { @@ -329,14 +380,24 @@ const visitors = { context.write('{:else if '); context.visit(node.test); context.write('}'); + + context.indent(); + context.newline(); context.visit(node.consequent); + context.dedent(); + context.newline(); } else { context.write('{#if '); context.visit(node.test); context.write('}'); + context.indent(); + context.newline(); context.visit(node.consequent); + context.dedent(); + context.newline(); } + if (node.alternate !== null) { if ( !( @@ -347,8 +408,14 @@ const visitors = { ) { context.write('{:else}'); } + + context.indent(); + context.newline(); context.visit(node.alternate); + context.dedent(); + context.newline(); } + if (!node.elseif) { context.write('{/if}'); } 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 index 776a6a4ca47f..7600adbcc92b 100644 --- a/packages/svelte/tests/print/test.ts +++ b/packages/svelte/tests/print/test.ts @@ -23,7 +23,7 @@ const { test, run } = suite(async (config, cwd) => { const file = `${cwd}/output.svelte`; const expected = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : ''; - assert.deepEqual(output.code, expected); + assert.deepEqual(output.code.trim(), expected.trim()); } }); From 4538f80259b8f1eca2b5086ecf0ee9c69712dd5a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 16:34:30 -0400 Subject: [PATCH 24/37] slightly nicer printing --- packages/svelte/src/compiler/print/index.js | 215 ++++++++++++-------- packages/svelte/tests/parser-modern/test.ts | 22 +- 2 files changed, 137 insertions(+), 100 deletions(-) diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index a387a31cace3..0e81cd6faf3a 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -16,6 +16,29 @@ export function print(ast) { }); } +/** + * @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) { @@ -54,81 +77,100 @@ const visitors = { } context.write('>'); - - context.indent(); - context.newline(); - context.visit(node.content); - context.dedent(); - context.newline(); - + block(context, node.content); context.write(''); }, Fragment(node, context) { - const join = context.new(); + /** @type {AST.SvelteNode[][]} */ + const items = []; - /** @type {Context[]} */ - const contexts = []; + /** @type {AST.SvelteNode[]} */ + let sequence = []; - let sequence = context.new(); - - let multiline = false; - - function flush() { - if (sequence.empty()) { - return; - } - - contexts.push(sequence); - sequence = context.new(); - } + const flush = () => { + items.push(sequence); + sequence = []; + }; for (let i = 0; i < node.nodes.length; i += 1) { - const child_node = node.nodes[i]; + let child_node = node.nodes[i]; + const prev = node.nodes[i - 1]; const next = node.nodes[i + 1]; - const prev_is_text = prev && (prev.type === 'Text' || prev.type === 'ExpressionTag'); - const next_is_text = next && (next.type === 'Text' || next.type === 'ExpressionTag'); + 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.type === 'Text' || child_node.type === 'ExpressionTag') { - if (child_node.type === 'Text') { - let { data } = child_node; + if (child_node.data === '') { + continue; + } + + if (child_node.data.startsWith(' ') && prev && prev.type !== 'ExpressionTag') { + flush(); + child_node.data = child_node.data.trimStart(); + } - let a = !prev_is_text && data !== (data = data.trimStart()); - let b = !next_is_text && data !== (data = data.trimEnd()); + if (child_node.data !== '') { + sequence.push({ ...child_node, data: child_node.data }); - if (data === '') { - if (prev && next) sequence.append(join); - } else { - if (a && prev) sequence.append(join); - sequence.write(data); - if (b && next) sequence.append(join); + if (child_node.data.endsWith(' ') && next && next.type !== 'ExpressionTag') { + flush(); + child_node.data = child_node.data.trimStart(); } - } else { - sequence.visit(child_node); } } else { - flush(); - const child_context = context.new(); - child_context.visit(child_node); + sequence.push(child_node); + } + } + + flush(); + + let multiline = false; + let width = 0; - contexts.push(child_context); + const child_contexts = items.map((sequence) => { + const child_context = context.new(); + for (const node of sequence) { + child_context.visit(node); multiline ||= child_context.multiline; } - } - flush(); + width += child_context.measure(); - if (multiline) { - join.newline(); - } else { - join.write(' '); - } + 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); - for (const child_context of contexts) { - context.append(child_context); + if (next) { + if (prev.multiline || next.multiline) { + context.margin(); + context.newline(); + } else if (multiline) { + context.newline(); + } else { + context.write(' '); + } + } } }, @@ -192,7 +234,7 @@ const visitors = { if (node.pending) { context.write('}'); - context.visit(node.pending); + block(context, node.pending); context.write('{:'); } else { context.write(' '); @@ -202,7 +244,8 @@ const visitors = { context.write(node.value ? 'then ' : 'then'); if (node.value) context.visit(node.value); context.write('}'); - context.visit(node.then); + + block(context, node.then); if (node.catch) { context.write('{:'); @@ -213,7 +256,8 @@ const visitors = { context.write(node.value ? 'catch ' : 'catch'); if (node.error) context.visit(node.error); context.write('}'); - context.visit(node.catch); + + block(context, node.catch); } context.write('{/await}'); @@ -303,7 +347,7 @@ const visitors = { if (node.fragment.nodes.length > 0) { context.write('>'); - context.visit(node.fragment); + block(context, node.fragment, true); context.write(``); } else { context.write(' />'); @@ -353,7 +397,8 @@ const visitors = { } context.write('}'); - context.visit(node.body); + + block(context, node.body); if (node.fallback) { context.write('{:else}'); @@ -381,21 +426,13 @@ const visitors = { context.visit(node.test); context.write('}'); - context.indent(); - context.newline(); - context.visit(node.consequent); - context.dedent(); - context.newline(); + block(context, node.consequent); } else { context.write('{#if '); context.visit(node.test); context.write('}'); - context.indent(); - context.newline(); - context.visit(node.consequent); - context.dedent(); - context.newline(); + block(context, node.consequent); } if (node.alternate !== null) { @@ -409,11 +446,7 @@ const visitors = { context.write('{:else}'); } - context.indent(); - context.newline(); - context.visit(node.alternate); - context.dedent(); - context.newline(); + block(context, node.alternate); } if (!node.elseif) { @@ -425,7 +458,7 @@ const visitors = { context.write('{#key '); context.visit(node.expression); context.write('}'); - context.visit(node.fragment); + block(context, node.fragment); context.write('{/key}'); }, @@ -487,24 +520,28 @@ const visitors = { }, RegularElement(node, context) { - context.write('<' + node.name); + const child_context = context.new(); + + child_context.write('<' + node.name); for (const attribute of node.attributes) { // TODO handle multiline - context.write(' '); - context.visit(attribute); + child_context.write(' '); + child_context.visit(attribute); } if (is_void(node.name)) { - context.write(' />'); + child_context.write(' />'); } else { - context.write('>'); + child_context.write('>'); if (node.fragment) { - context.visit(node.fragment); - context.write(``); + block(child_context, node.fragment, child_context.measure() < 30); + child_context.write(``); } } + + context.append(child_context); }, RelativeSelector(node, context) { @@ -566,7 +603,7 @@ const visitors = { if (node.fragment.nodes.length > 0) { context.write('>'); - context.visit(node.fragment); + context.visit(node.fragment); // TODO block/inline context.write(''); } else { context.write(' />'); @@ -589,7 +626,7 @@ const visitors = { } context.write(')}'); - context.visit(node.body); + block(context, node.body); context.write('{/snippet}'); }, @@ -658,7 +695,7 @@ const visitors = { if (node.fragment) { context.write('>'); - context.visit(node.fragment); + block(context, node.fragment, true); context.write(``); } else { context.write('/>'); @@ -680,7 +717,7 @@ const visitors = { if (node.fragment) { context.write('>'); - context.visit(node.fragment); + block(context, node.fragment, true); context.write(``); } else { context.write('/>'); @@ -698,7 +735,7 @@ const visitors = { if (node.fragment) { context.write('>'); - context.visit(node.fragment); + block(context, node.fragment, true); context.write(``); } else { context.write('/>'); @@ -720,7 +757,7 @@ const visitors = { if (node.fragment) { context.write('>'); - context.visit(node.fragment); + block(context, node.fragment, true); context.write(``); } else { context.write('/>'); @@ -738,7 +775,7 @@ const visitors = { if (node.fragment) { context.write('>'); - context.visit(node.fragment); + block(context, node.fragment, true); context.write(``); } else { context.write('/>'); @@ -756,7 +793,7 @@ const visitors = { if (node.fragment) { context.write('>'); - context.visit(node.fragment); + block(context, node.fragment, true); context.write(``); } else { context.write('/>'); @@ -774,7 +811,7 @@ const visitors = { if (node.fragment) { context.write('>'); - context.visit(node.fragment); + block(context, node.fragment, true); context.write(``); } else { context.write('/>'); @@ -792,7 +829,7 @@ const visitors = { if (node.fragment) { context.write('>'); - context.visit(node.fragment); + block(context, node.fragment, true); context.write(``); } else { context.write('/>'); diff --git a/packages/svelte/tests/parser-modern/test.ts b/packages/svelte/tests/parser-modern/test.ts index 4130bd8d613e..4f9bae4a056c 100644 --- a/packages/svelte/tests/parser-modern/test.ts +++ b/packages/svelte/tests/parser-modern/test.ts @@ -87,23 +87,23 @@ function clean(ast: AST.SvelteNode) { let child = node.nodes[i]; if (child.type === 'Text') { + child = { + ...child, + data: child.data.replace(/[^\S]+/g, ' '), + raw: child.raw.replace(/[^\S]+/g, ' ') + }; + if (i === 0) { - child = { - ...child, - data: child.data.trimStart(), - raw: child.raw.trimStart() - }; + child.data = child.data.trimStart(); + child.raw = child.raw.trimStart(); } if (i === node.nodes.length - 1) { - child = { - ...child, - data: child.data.trimEnd(), - raw: child.raw.trimEnd() - }; + child.data = child.data.trimEnd(); + child.raw = child.raw.trimEnd(); } - if (!child.data) continue; + if (child.data === '') continue; } nodes.push(context.visit(child)); From e47f5794d0633448ab0bb62e0f820ef485ffe524 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 16:50:47 -0400 Subject: [PATCH 25/37] install from pkg.pr.new --- package.json | 8 -------- packages/svelte/package.json | 2 +- pnpm-lock.yaml | 19 ++++++++++--------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 4f5748cc5dd2..62581782d72d 100644 --- a/package.json +++ b/package.json @@ -42,13 +42,5 @@ "typescript-eslint": "^8.24.0", "v8-natives": "^1.2.5", "vitest": "^2.1.9" - }, - "pnpm": { - "overrides": { - "esrap": "link:../../esrap" - } - }, - "dependencies": { - "esrap": "link:../../../../esrap" } } diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1b1276182b55..4a294f923eb4 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -171,7 +171,7 @@ "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", - "esrap": "https://pkg.pr.new/sveltejs/esrap@a275a5c", + "esrap": "https://pkg.pr.new/sveltejs/esrap@b5ba3da", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08373e6b9d36..eb1b3810ebfc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,16 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - esrap: link:../../esrap - importers: .: - dependencies: - esrap: - specifier: link:../../esrap - version: link:../../esrap devDependencies: '@changesets/cli': specifier: ^2.27.8 @@ -94,8 +87,8 @@ importers: specifier: ^1.2.1 version: 1.2.1 esrap: - specifier: link:../../../../esrap - version: link:../../../../esrap + specifier: https://pkg.pr.new/sveltejs/esrap@b5ba3da + version: https://pkg.pr.new/sveltejs/esrap@b5ba3da is-reference: specifier: ^3.0.3 version: 3.0.3 @@ -1268,6 +1261,10 @@ packages: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} + esrap@https://pkg.pr.new/sveltejs/esrap@b5ba3da: + resolution: {tarball: https://pkg.pr.new/sveltejs/esrap@b5ba3da} + version: 1.4.9 + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -3626,6 +3623,10 @@ snapshots: dependencies: estraverse: 5.3.0 + esrap@https://pkg.pr.new/sveltejs/esrap@b5ba3da: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 From df3be44c276a68c28c4708b1952cea2e24ef696d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 16:54:23 -0400 Subject: [PATCH 26/37] merge main --- .../imports-in-modules/_expected/client/index.svelte.js | 2 +- .../imports-in-modules/_expected/client/module.svelte.js | 2 +- .../imports-in-modules/_expected/server/index.svelte.js | 2 +- .../imports-in-modules/_expected/server/module.svelte.js | 2 +- .../snapshot/samples/purity/_expected/client/index.svelte.js | 4 +--- 5 files changed, 5 insertions(+), 7 deletions(-) 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 ad6beb0c7b0c..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,4 +3,4 @@ import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; import { random } from './module.svelte'; -export default function Imports_in_modules($$anchor) {} +export default function Imports_in_modules($$anchor) {} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/module.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/module.svelte.js index 0afcd51e4b12..0d366e6258ff 100644 --- a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/module.svelte.js +++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client/module.svelte.js @@ -2,4 +2,4 @@ import * as $ from 'svelte/internal/client'; import { random } from './export'; -export { random }; +export { random }; \ 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 da97f06aada3..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,4 +1,4 @@ import * as $ from 'svelte/internal/server'; import { random } from './module.svelte'; -export default function Imports_in_modules($$payload) {} +export default function Imports_in_modules($$payload) {} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/module.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/module.svelte.js index d57d7c2d5a01..2e0af8af84d8 100644 --- a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/module.svelte.js +++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/server/module.svelte.js @@ -2,4 +2,4 @@ import * as $ from 'svelte/internal/server'; import { random } from './export'; -export { random }; +export { random }; \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js index da6fdf44d881..f3a93432da7a 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js @@ -8,9 +8,7 @@ export default function Purity($$anchor) { var fragment = root(); var p = $.first_child(fragment); - p.textContent = ( - $.untrack(() => Math.max(0, Math.min(0, 100))) - ); + p.textContent = ($.untrack(() => Math.max(0, Math.min(0, 100)))); var p_1 = $.sibling(p, 2); From c474d679e9e1be5f4e985ea7ef31fc5184b6d2c9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 17:55:18 -0400 Subject: [PATCH 27/37] bump --- packages/svelte/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/svelte/package.json b/packages/svelte/package.json index cc04eda4da8f..cced2562cb17 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -171,7 +171,7 @@ "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", - "esrap": "https://pkg.pr.new/sveltejs/esrap@b5ba3da", + "esrap": "https://pkg.pr.new/sveltejs/esrap@ef4051a", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb1b3810ebfc..f722e5e939b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,8 +87,8 @@ importers: specifier: ^1.2.1 version: 1.2.1 esrap: - specifier: https://pkg.pr.new/sveltejs/esrap@b5ba3da - version: https://pkg.pr.new/sveltejs/esrap@b5ba3da + specifier: https://pkg.pr.new/sveltejs/esrap@ef4051a + version: https://pkg.pr.new/sveltejs/esrap@ef4051a is-reference: specifier: ^3.0.3 version: 3.0.3 @@ -1261,8 +1261,8 @@ packages: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} - esrap@https://pkg.pr.new/sveltejs/esrap@b5ba3da: - resolution: {tarball: https://pkg.pr.new/sveltejs/esrap@b5ba3da} + esrap@https://pkg.pr.new/sveltejs/esrap@ef4051a: + resolution: {tarball: https://pkg.pr.new/sveltejs/esrap@ef4051a} version: 1.4.9 esrecurse@4.3.0: @@ -3623,7 +3623,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@https://pkg.pr.new/sveltejs/esrap@b5ba3da: + esrap@https://pkg.pr.new/sveltejs/esrap@ef4051a: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 From e904753ecc96d5a2a1bac282044f1c8e63257e87 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Jun 2025 18:16:13 -0400 Subject: [PATCH 28/37] fix --- packages/svelte/package.json | 2 +- .../svelte/scripts/process-messages/index.js | 2 -- .../src/compiler/phases/1-parse/acorn.js | 7 ++++- packages/svelte/src/compiler/print/index.js | 27 ++++++++++++------- .../svelte/src/compiler/types/template.d.ts | 2 +- pnpm-lock.yaml | 10 +++---- 6 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/svelte/package.json b/packages/svelte/package.json index cced2562cb17..1cc5870f43be 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -171,7 +171,7 @@ "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", - "esrap": "https://pkg.pr.new/sveltejs/esrap@ef4051a", + "esrap": "https://pkg.pr.new/sveltejs/esrap@17c22f5", "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 d246cfbba5c4..80bf0bade626 100644 --- a/packages/svelte/scripts/process-messages/index.js +++ b/packages/svelte/scripts/process-messages/index.js @@ -147,7 +147,6 @@ function run() { const printed = esrap.print( ast, - // @ts-expect-error ts({ comments: comments.filter((comment) => comment !== jsdoc) }) @@ -358,7 +357,6 @@ function run() { .join('\n') }; - // @ts-expect-error const block = esrap.print({ ...ast, body: [clone] }, ts({ comments: [jsdoc_clone] })).code; printed.code += `\n\n${block}`; diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index f28ceab4cec8..6fd7fcaf4d85 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -107,6 +107,8 @@ function get_comment_handlers(source, comments, index = 0) { * @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, start_loc, end_loc) => { if (block && /\n/.test(value)) { @@ -125,7 +127,10 @@ function get_comment_handlers(source, comments, index = 0) { value, start, end, - loc: { start: start_loc, end: end_loc } + loc: { + start: /** @type {import('acorn').Position} */ (start_loc), + end: /** @type {import('acorn').Position} */ (end_loc) + } }); }, diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 0e81cd6faf3a..afa355663253 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -10,7 +10,6 @@ import { is_void } from '../../utils.js'; export function print(ast) { // @ts-expect-error some bullshit return esrap.print(ast, { - // @ts-expect-error some bullshit ...ts({ comments: ast.type === 'Root' ? ast.comments : [] }), ...visitors }); @@ -641,13 +640,23 @@ const visitors = { 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('}'); + + 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); } }, @@ -746,7 +755,7 @@ const visitors = { context.write('` element, if exists */ module: Script | null; /** Comments found in