From 97c5d98f7944666ecf9de57e9088ba288ca1e67a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 16 Jun 2025 16:59:26 -0400 Subject: [PATCH] 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 ac0797927e..11db091936 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 26a09abb66..5a1e693bdc 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 6cc5b58aa6..b8ae8199eb 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 b118901830..282288e2a2 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -59,7 +59,12 @@ export default function read_pattern(parser) { space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1); const expression = /** @type {any} */ ( - parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1) + parse_expression_at( + `${space_with_newline}(${pattern_string} = 1)`, + parser.root.comments, + parser.ts, + start - 1 + ) ).left; expression.typeAnnotation = read_type_annotation(parser); @@ -96,13 +101,13 @@ function read_type_annotation(parser) { // parameters as part of a sequence expression instead, and will then error on optional // parameters (`?:`). Therefore replace that sequence with something that will not error. parser.template.slice(parser.index).replace(/\?\s*:/g, ':'); - let expression = parse_expression_at(template, parser.ts, a); + let expression = parse_expression_at(template, parser.root.comments, parser.ts, a); // `foo: bar = baz` gets mangled — fix it if (expression.type === 'AssignmentExpression') { let b = expression.right.start; while (template[b] !== '=') b -= 1; - expression = parse_expression_at(template.slice(0, b), parser.ts, a); + expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a); } // `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js index a596cdf572..bad0c4ae96 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 6290127811..9ce449f200 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/script.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js @@ -34,7 +34,7 @@ export function read_script(parser, start, attributes) { let ast; try { - ast = acorn.parse(source, parser.ts, true); + ast = acorn.parse(source, parser.root.comments, parser.ts, true); } catch (err) { parser.acorn_error(err); } diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 4153463c83..f86b7bfec6 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 fded183b86..530089dd67 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 e2e006c14b..e85a35cf8e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -362,6 +362,9 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); + // trick esrap into including comments + component_block.loc = instance.loc; + if (!analysis.runes) { // Bind static exports to props so that people can access them with bind:x for (const { name, alias } of analysis.exports) { diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index 2d045bf363..ae49470d7c 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 7a3d6bef6c..86346b864c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -242,6 +242,9 @@ export function server_component(analysis, options) { .../** @type {Statement[]} */ (template.body) ]); + // trick esrap into including comments + component_block.loc = instance.loc; + if (analysis.props_id) { // need to be placed on first line of the component for hydration component_block.body.unshift( diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 67cbd75ff8..aeb6184724 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 d3c6f31c77..bf17b4c1e6 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 7d2a0397b1..60f6ec3bdb 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 `