From a0f86c5e1b77f4768793ac918edc87631486c7db Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 15 Jun 2025 15:41:51 -0400 Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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/17] 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/17] `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/17] 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/17] `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/17] `{#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/17] 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/17] 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/17] 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/17] `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/17] `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/17] 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 {