From 25d0bbf4e4dcd2c95deeca1cd87127b8fe93442f Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sat, 28 Jun 2025 11:03:57 +0200 Subject: [PATCH 01/24] chore: update `esrap` --- packages/core/package.json | 2 +- packages/core/tests/js/index.ts | 4 +- packages/core/tests/utils.ts | 64 ++------------------- packages/core/tooling/index.ts | 91 +++++++++--------------------- packages/core/tooling/js/common.ts | 16 +++--- packages/core/tooling/parsers.ts | 10 ++-- pnpm-lock.yaml | 11 +++- 7 files changed, 59 insertions(+), 139 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 8897e9904..cc4707e72 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -51,7 +51,7 @@ "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", - "esrap": "^1.4.9", + "esrap": "^2.0.0", "htmlparser2": "^9.1.0", "magic-string": "^0.30.17", "picocolors": "^1.1.1", diff --git a/packages/core/tests/js/index.ts b/packages/core/tests/js/index.ts index c7f2021e6..9aca4de6a 100644 --- a/packages/core/tests/js/index.ts +++ b/packages/core/tests/js/index.ts @@ -16,13 +16,13 @@ for (const categoryDirectory of categoryDirectories) { const inputFilePath = join(testDirectoryPath, 'input.ts'); const input = fs.existsSync(inputFilePath) ? fs.readFileSync(inputFilePath, 'utf8') : ''; - const ast = parseScript(input); + const { ast, comments } = parseScript(input); // dynamic imports always need to provide the path inline for static analysis const module = await import(`./${categoryDirectory}/${testName}/run.ts`); module.run(ast); - let output = serializeScript(ast, input); + let output = serializeScript(ast, comments, input); if (!output.endsWith('\n')) output += '\n'; await expect(output).toMatchFileSnapshot(`${testDirectoryPath}/output.ts`); }); diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index 37979d0be..ef3272504 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -4,7 +4,6 @@ import { parseScript, serializeScript, guessIndentString, - guessQuoteStyle, type AstTypes } from '../tooling/index.ts'; @@ -48,57 +47,6 @@ test('guessIndentString - eight spaces', () => { expect(guessIndentString(code)).toBe(' '); }); -test('guessQuoteStyle - single simple', () => { - const code = dedent` - console.log('asd'); - `; - const ast = parseScript(code); - - expect(guessQuoteStyle(ast)).toBe('single'); -}); - -test('guessQuoteStyle - single complex', () => { - const code = dedent` - import foo from 'bar'; - - console.log("bar"); - const foobar = 'foo'; - `; - const ast = parseScript(code); - - expect(guessQuoteStyle(ast)).toBe('single'); -}); - -test('guessQuoteStyle - double simple', () => { - const code = dedent` - console.log("asd"); - `; - const ast = parseScript(code); - - expect(guessQuoteStyle(ast)).toBe('double'); -}); - -test('guessQuoteStyle - double complex', () => { - const code = dedent` - import foo from 'bar'; - - console.log("bar"); - const foobar = "foo"; - `; - const ast = parseScript(code); - - expect(guessQuoteStyle(ast)).toBe('double'); -}); - -test('guessQuoteStyle - no quotes', () => { - const code = dedent` - const foo = true; - `; - const ast = parseScript(code); - - expect(guessQuoteStyle(ast)).toBe(undefined); -}); - const newVariableDeclaration: AstTypes.VariableDeclaration = { type: 'VariableDeclaration', kind: 'const', @@ -126,13 +74,13 @@ test('integration - simple', () => { const foobar = "foo"; } `; - const ast = parseScript(code); + const { ast, comments } = parseScript(code); const method = ast.body[1] as AstTypes.FunctionDeclaration; method.body.body.push(newVariableDeclaration); // new variable is added with correct indentation and matching quotes - expect(serializeScript(ast, code)).toMatchInlineSnapshot(` + expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(` "import foo from 'bar'; function bar() { @@ -153,13 +101,13 @@ test('integration - simple 2', () => { const foobar = 'foo'; } `; - const ast = parseScript(code); + const { ast, comments } = parseScript(code); const method = ast.body[1] as AstTypes.FunctionDeclaration; method.body.body.push(newVariableDeclaration); // new variable is added with correct indentation and matching quotes - expect(serializeScript(ast, code)).toMatchInlineSnapshot(` + expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(` "import foo from 'bar'; function bar() { @@ -176,9 +124,9 @@ test('integration - preserves comments', () => { /** @type {string} */ let foo = 'bar'; `; - const ast = parseScript(code); + const { ast, comments } = parseScript(code); - expect(serializeScript(ast, code)).toMatchInlineSnapshot(` + expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(` "/** @type {string} */ let foo = 'bar';" `); diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index ac42aa48f..450666edb 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -14,6 +14,7 @@ import { } from 'postcss'; import * as fleece from 'silver-fleece'; import { print as esrapPrint } from 'esrap'; +import ts from 'esrap/languages/ts'; import * as acorn from 'acorn'; import { tsPlugin } from '@sveltejs/acorn-typescript'; @@ -47,19 +48,21 @@ export type { /** * Parses as string to an AST. Code below is taken from `esrap` to ensure compatibilty. - * https://github.com/sveltejs/esrap/blob/9daf5dd43b31f17f596aa7da91678f2650666dd0/test/common.js#L12 + * https://github.com/sveltejs/esrap/blob/920491535d31484ac5fae2327c7826839d851aed/test/common.js#L14 */ -export function parseScript(content: string): TsEstree.Program { +export function parseScript(content: string): { + ast: TsEstree.Program; + comments: TsEstree.Comment[]; +} { const comments: TsEstree.Comment[] = []; const acornTs = acorn.Parser.extend(tsPlugin()); - // Acorn doesn't add comments to the AST by itself. This factory returns the capabilities to add them after the fact. const ast = acornTs.parse(content, { ecmaVersion: 'latest', sourceType: 'module', locations: true, - onComment: (block, value, start, end) => { + onComment: (block, value, start, end, startLoc, endLoc) => { if (block && /\n/.test(value)) { let a = start; while (a > 0 && content[a - 1] !== '\n') a -= 1; @@ -71,38 +74,31 @@ export function parseScript(content: string): TsEstree.Program { 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: startLoc as TsEstree.Position, end: endLoc as TsEstree.Position } + }); } }) as TsEstree.Program; - Walker.walk(ast as TsEstree.Node, null, { - _(commentNode, { next }) { - let comment: TsEstree.Comment; - - while (comments[0] && commentNode.start && comments[0].start! < commentNode.start) { - comment = comments.shift()!; - (commentNode.leadingComments ??= []).push(comment); - } - - next(); - - if (comments[0]) { - const slice = content.slice(commentNode.end, comments[0].start); - - if (/^[,) \t]*$/.test(slice)) { - commentNode.trailingComments = [comments.shift()!]; - } - } - } - }); - - return ast; + return { + ast, + comments + }; } -export function serializeScript(ast: TsEstree.Node, previousContent?: string): string { - const { code } = esrapPrint(ast, { - indent: guessIndentString(previousContent), - quotes: guessQuoteStyle(ast) +export function serializeScript( + ast: TsEstree.Node, + comments: TsEstree.Comment[], + previousContent?: string +): string { + // @ts-expect-error we are still using `estree` while `esrap` is using `@typescript-eslint/types` + // which is causing these errors. But they are simmilar enough to work together. + const { code } = esrapPrint(ast, ts({ comments }), { + indent: guessIndentString(previousContent) }); return code; } @@ -205,36 +201,3 @@ export function guessIndentString(str: string | undefined): string { return '\t'; } } - -export function guessQuoteStyle(ast: TsEstree.Node): 'single' | 'double' | undefined { - let singleCount = 0; - let doubleCount = 0; - - Walker.walk(ast, null, { - Literal(node) { - if (node.raw && node.raw.length >= 2) { - // we have at least two characters in the raw string that could represent both quotes - const quotes = [node.raw[0], node.raw[node.raw.length - 1]]; - for (const quote of quotes) { - switch (quote) { - case "'": - singleCount++; - break; - case '"': - doubleCount++; - break; - default: - break; - } - } - } - } - }); - - if (singleCount === 0 && doubleCount === 0) { - // new file or file without any quotes - return undefined; - } - - return singleCount > doubleCount ? 'single' : 'double'; -} diff --git a/packages/core/tooling/js/common.ts b/packages/core/tooling/js/common.ts index 8812660e2..d249b693b 100644 --- a/packages/core/tooling/js/common.ts +++ b/packages/core/tooling/js/common.ts @@ -93,7 +93,7 @@ export function areNodesEqual(node: AstTypes.Node, otherNode: AstTypes.Node): bo const nodeClone = stripAst(decircular(node), ['loc', 'raw']); const otherNodeClone = stripAst(decircular(otherNode), ['loc', 'raw']); - return serializeScript(nodeClone) === serializeScript(otherNodeClone); + return serializeScript(nodeClone, []) === serializeScript(otherNodeClone, []); } export function createBlockStatement(): AstTypes.BlockStatement { @@ -118,18 +118,18 @@ export function appendFromString( node: AstTypes.BlockStatement | AstTypes.Program, options: { code: string } ): void { - const program = parseScript(dedent(options.code)); + const { ast } = parseScript(dedent(options.code)); - for (const childNode of program.body) { + for (const childNode of ast.body) { // @ts-expect-error node.body.push(childNode); } } export function parseExpression(code: string): AstTypes.Expression { - const program = parseScript(dedent(code)); - stripAst(program, ['raw']); - const statement = program.body[0]!; + const { ast } = parseScript(dedent(code)); + stripAst(ast, ['raw']); + const statement = ast.body[0]!; if (statement.type !== 'ExpressionStatement') { throw new Error('Code provided was not an expression'); } @@ -142,8 +142,8 @@ export function parseStatement(code: string): AstTypes.Statement { } export function parseFromString(code: string): T { - const program = parseScript(dedent(code)); - const statement = program.body[0]!; + const { ast } = parseScript(dedent(code)); + const statement = ast.body[0]!; return statement as T; } diff --git a/packages/core/tooling/parsers.ts b/packages/core/tooling/parsers.ts index f7764d34f..7d39bad23 100644 --- a/packages/core/tooling/parsers.ts +++ b/packages/core/tooling/parsers.ts @@ -6,11 +6,13 @@ type ParseBase = { generateCode(): string; }; -export function parseScript(source: string): { ast: utils.AstTypes.Program } & ParseBase { - const ast = utils.parseScript(source); - const generateCode = () => utils.serializeScript(ast, source); +export function parseScript( + source: string +): { ast: utils.AstTypes.Program; comments: utils.AstTypes.Comment[] } & ParseBase { + const { ast, comments } = utils.parseScript(source); + const generateCode = () => utils.serializeScript(ast, comments, source); - return { ast, source, generateCode }; + return { ast, comments, source, generateCode }; } export function parseCss(source: string): { ast: utils.CssAst } & ParseBase { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 959d7df38..0f3596458 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,8 +170,8 @@ importers: specifier: ^3.2.2 version: 3.2.2 esrap: - specifier: ^1.4.9 - version: 1.4.9 + specifier: ^2.0.0 + version: 2.0.1 htmlparser2: specifier: ^9.1.0 version: 9.1.0 @@ -1299,6 +1299,9 @@ packages: esrap@1.4.9: resolution: {integrity: sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==} + esrap@2.0.1: + resolution: {integrity: sha512-6n1JodkxeMvyTDCog7J//t8Yti//fGicZgtFLko6h/aEpc54BK9O8k9cZgC2J8+2Dh1U5uYIxuJWSsylybvFBA==} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -3298,6 +3301,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + esrap@2.0.1: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 From 9394c80a3803bc60dadb2ff40636509e620d1766 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 26 Oct 2025 06:55:29 +0100 Subject: [PATCH 02/24] use esrap pkg.pr.new --- packages/core/package.json | 2 +- packages/core/tooling/index.ts | 7 ++++--- pnpm-lock.yaml | 11 ++++++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index cc4707e72..350e3fdcf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -51,7 +51,7 @@ "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", - "esrap": "^2.0.0", + "esrap": "https://pkg.pr.new/sveltejs/esrap@718afce", "htmlparser2": "^9.1.0", "magic-string": "^0.30.17", "picocolors": "^1.1.1", diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index 450666edb..1d56b51e8 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -14,7 +14,7 @@ import { } from 'postcss'; import * as fleece from 'silver-fleece'; import { print as esrapPrint } from 'esrap'; -import ts from 'esrap/languages/ts'; +import ts, { type AdditionalComment } from 'esrap/languages/ts'; import * as acorn from 'acorn'; import { tsPlugin } from '@sveltejs/acorn-typescript'; @@ -93,11 +93,12 @@ export function parseScript(content: string): { export function serializeScript( ast: TsEstree.Node, comments: TsEstree.Comment[], - previousContent?: string + previousContent?: string, + additionalComments?: WeakMap ): string { // @ts-expect-error we are still using `estree` while `esrap` is using `@typescript-eslint/types` // which is causing these errors. But they are simmilar enough to work together. - const { code } = esrapPrint(ast, ts({ comments }), { + const { code } = esrapPrint(ast, ts({ comments, additionalComments }), { indent: guessIndentString(previousContent) }); return code; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b9000f76..af8676d00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,8 +170,8 @@ importers: specifier: ^3.2.2 version: 3.2.2 esrap: - specifier: ^2.0.0 - version: 2.0.1 + specifier: https://pkg.pr.new/sveltejs/esrap@718afce + version: https://pkg.pr.new/sveltejs/esrap@718afce htmlparser2: specifier: ^9.1.0 version: 9.1.0 @@ -1301,8 +1301,9 @@ packages: esrap@1.4.9: resolution: {integrity: sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==} - esrap@2.0.1: - resolution: {integrity: sha512-6n1JodkxeMvyTDCog7J//t8Yti//fGicZgtFLko6h/aEpc54BK9O8k9cZgC2J8+2Dh1U5uYIxuJWSsylybvFBA==} + esrap@https://pkg.pr.new/sveltejs/esrap@718afce: + resolution: {tarball: https://pkg.pr.new/sveltejs/esrap@718afce} + version: 2.1.0 esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -3306,7 +3307,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - esrap@2.0.1: + esrap@https://pkg.pr.new/sveltejs/esrap@718afce: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 From 79f262eef3b6486b711787f2b7e69585d8e071b5 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 26 Oct 2025 06:55:54 +0100 Subject: [PATCH 03/24] fix `svelte-kit` adapter addon --- packages/addons/sveltekit-adapter/index.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/addons/sveltekit-adapter/index.ts b/packages/addons/sveltekit-adapter/index.ts index 3d8d9fc7c..f3ae2bcf9 100644 --- a/packages/addons/sveltekit-adapter/index.ts +++ b/packages/addons/sveltekit-adapter/index.ts @@ -49,7 +49,7 @@ export default defineAddon({ sv.devDependency(adapter.package, adapter.version); sv.file('svelte.config.js', (content) => { - const { ast, generateCode } = parseScript(content); + const { ast, comments, generateCode } = parseScript(content); // finds any existing adapter's import declaration const importDecls = ast.body.filter((n) => n.type === 'ImportDeclaration'); @@ -79,12 +79,20 @@ export default defineAddon({ ) as AstTypes.Property | undefined; if (kitConfig && kitConfig.value.type === 'ObjectExpression') { - const adapterProp = kitConfig.value.properties.find( - (p) => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'adapter' + // removes any existing adapter auto comments + const adapterAutoComments = comments.filter( + (c) => + c.loc && + kitConfig.loc && + c.loc.start.line >= kitConfig.loc.start.line && + c.loc.end.line <= kitConfig.loc.end.line + ); + // modify the array in place + comments.splice( + 0, + comments.length, + ...comments.filter((c) => !adapterAutoComments.includes(c)) ); - if (adapterProp) { - adapterProp.leadingComments = []; - } // only overrides the `adapter` property so we can reset it's args object.overrideProperties(kitConfig.value, { From 7c0c0d2592a586291de0af0a4c4ec4db6be5eede Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 26 Oct 2025 07:00:44 +0100 Subject: [PATCH 04/24] remove useless comment --- packages/core/tests/js/object/create/run.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/tests/js/object/create/run.ts b/packages/core/tests/js/object/create/run.ts index 8bdd272f5..5b19937e0 100644 --- a/packages/core/tests/js/object/create/run.ts +++ b/packages/core/tests/js/object/create/run.ts @@ -37,6 +37,5 @@ export function run(ast: AstTypes.Program): void { name: 'created2', value: createdObject2 }); - createdVariable2.leadingComments = [{ type: 'Line', value: ' prettier-ignore' }]; ast.body.push(createdVariable2); } From f41da148e0ea805c8643189efeff7406655e8f8b Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 26 Oct 2025 07:08:34 +0100 Subject: [PATCH 05/24] fix eslint --- packages/addons/eslint/index.ts | 12 +++++++----- packages/core/tooling/index.ts | 4 +++- packages/core/tooling/parsers.ts | 16 ++++++++++------ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/addons/eslint/index.ts b/packages/addons/eslint/index.ts index e0130e5b4..80424282d 100644 --- a/packages/addons/eslint/index.ts +++ b/packages/addons/eslint/index.ts @@ -52,7 +52,7 @@ export default defineAddon({ }); sv.file('eslint.config.js', (content) => { - const { ast, generateCode } = parseScript(content); + const { ast, additionalComments, generateCode } = parseScript(content); const eslintConfigs: Array = []; imports.addDefault(ast, { from: './svelte.config.js', as: 'svelteConfig' }); @@ -84,18 +84,20 @@ export default defineAddon({ if (rules.properties[0].type !== 'Property') { throw new Error('rules.properties[0].type !== "Property"'); } - rules.properties[0].key.leadingComments = [ + additionalComments.set(rules.properties[0].key, [ { type: 'Line', value: - ' typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.' + ' typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.', + position: 'leading' }, { type: 'Line', value: - ' see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors' + ' see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors', + position: 'leading' } - ]; + ]); const globalsConfig = object.create({ languageOptions: { diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index 1d56b51e8..181270aa1 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -53,6 +53,7 @@ export type { export function parseScript(content: string): { ast: TsEstree.Program; comments: TsEstree.Comment[]; + additionalComments: WeakMap; } { const comments: TsEstree.Comment[] = []; @@ -86,7 +87,8 @@ export function parseScript(content: string): { return { ast, - comments + comments, + additionalComments: new WeakMap() }; } diff --git a/packages/core/tooling/parsers.ts b/packages/core/tooling/parsers.ts index 7d39bad23..cd6f3b568 100644 --- a/packages/core/tooling/parsers.ts +++ b/packages/core/tooling/parsers.ts @@ -1,18 +1,22 @@ import * as utils from './index.ts'; import MagicString from 'magic-string'; +import type { TsEstree } from './js/ts-estree.ts'; +import type { AdditionalComment } from 'esrap/languages/ts'; type ParseBase = { source: string; generateCode(): string; }; -export function parseScript( - source: string -): { ast: utils.AstTypes.Program; comments: utils.AstTypes.Comment[] } & ParseBase { - const { ast, comments } = utils.parseScript(source); - const generateCode = () => utils.serializeScript(ast, comments, source); +export function parseScript(source: string): { + ast: utils.AstTypes.Program; + comments: utils.AstTypes.Comment[]; + additionalComments: WeakMap; +} & ParseBase { + const { ast, comments, additionalComments } = utils.parseScript(source); + const generateCode = () => utils.serializeScript(ast, comments, source, additionalComments); - return { ast, comments, source, generateCode }; + return { ast, comments, additionalComments, source, generateCode }; } export function parseCss(source: string): { ast: utils.CssAst } & ParseBase { From 7307cc4d6333e22fbead282ffd6edb300cb49076 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 26 Oct 2025 07:35:06 +0100 Subject: [PATCH 06/24] fix eslint --- packages/addons/eslint/index.ts | 4 +- .../core/tests/js/common/jsdoc-comment/run.ts | 6 +-- .../tests/js/common/jsdoc-type-comment/run.ts | 6 +-- packages/core/tests/js/index.ts | 6 +-- packages/core/tooling/index.ts | 7 +++- packages/core/tooling/js/common.ts | 41 ++++++++++++++----- packages/core/tooling/js/index.ts | 2 +- packages/core/tooling/parsers.ts | 4 +- 8 files changed, 49 insertions(+), 27 deletions(-) diff --git a/packages/addons/eslint/index.ts b/packages/addons/eslint/index.ts index 80424282d..d20dd0df2 100644 --- a/packages/addons/eslint/index.ts +++ b/packages/addons/eslint/index.ts @@ -154,7 +154,9 @@ export default defineAddon({ // type annotate config if (!typescript) - common.addJsDocTypeComment(astNode, { type: "import('eslint').Linter.Config[]" }); + common.addJsDocTypeComment(astNode, additionalComments, { + type: "import('eslint').Linter.Config[]" + }); if (typescript) imports.addDefault(ast, { from: 'typescript-eslint', as: 'ts' }); imports.addNamed(ast, { from: 'node:url', imports: ['fileURLToPath'] }); diff --git a/packages/core/tests/js/common/jsdoc-comment/run.ts b/packages/core/tests/js/common/jsdoc-comment/run.ts index c0b6c06f1..15d0369b9 100644 --- a/packages/core/tests/js/common/jsdoc-comment/run.ts +++ b/packages/core/tests/js/common/jsdoc-comment/run.ts @@ -1,9 +1,9 @@ -import { common, type AstTypes } from '@sveltejs/cli-core/js'; +import { common, type AdditionalCommentMap, type AstTypes } from '@sveltejs/cli-core/js'; -export function run(ast: AstTypes.Program): void { +export function run(ast: AstTypes.Program, additionalComments: AdditionalCommentMap): void { const functionDeclaration = ast.body[0] as AstTypes.FunctionDeclaration; - common.addJsDocComment(functionDeclaration, { + common.addJsDocComment(functionDeclaration, additionalComments, { params: { 'import("$lib/paraglide/runtime").AvailableLanguageTag': 'newLanguage' } }); } diff --git a/packages/core/tests/js/common/jsdoc-type-comment/run.ts b/packages/core/tests/js/common/jsdoc-type-comment/run.ts index 9b5e5a065..5074afd58 100644 --- a/packages/core/tests/js/common/jsdoc-type-comment/run.ts +++ b/packages/core/tests/js/common/jsdoc-type-comment/run.ts @@ -1,13 +1,13 @@ -import { common, variables, type AstTypes } from '@sveltejs/cli-core/js'; +import { common, variables, type AdditionalCommentMap, type AstTypes } from '@sveltejs/cli-core/js'; -export function run(ast: AstTypes.Program): void { +export function run(ast: AstTypes.Program, additionalComments: AdditionalCommentMap): void { const declaration = variables.declaration(ast, { kind: 'const', name: 'foo', value: { type: 'Literal', value: 42 } }); - common.addJsDocTypeComment(declaration, { + common.addJsDocTypeComment(declaration, additionalComments, { type: 'number' }); diff --git a/packages/core/tests/js/index.ts b/packages/core/tests/js/index.ts index 9aca4de6a..bc32a5c83 100644 --- a/packages/core/tests/js/index.ts +++ b/packages/core/tests/js/index.ts @@ -16,13 +16,13 @@ for (const categoryDirectory of categoryDirectories) { const inputFilePath = join(testDirectoryPath, 'input.ts'); const input = fs.existsSync(inputFilePath) ? fs.readFileSync(inputFilePath, 'utf8') : ''; - const { ast, comments } = parseScript(input); + const { ast, comments, additionalComments } = parseScript(input); // dynamic imports always need to provide the path inline for static analysis const module = await import(`./${categoryDirectory}/${testName}/run.ts`); - module.run(ast); + module.run(ast, additionalComments); - let output = serializeScript(ast, comments, input); + let output = serializeScript(ast, comments, input, additionalComments); if (!output.endsWith('\n')) output += '\n'; await expect(output).toMatchFileSnapshot(`${testDirectoryPath}/output.ts`); }); diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index 181270aa1..d2f1aa0f3 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -18,6 +18,8 @@ import ts, { type AdditionalComment } from 'esrap/languages/ts'; import * as acorn from 'acorn'; import { tsPlugin } from '@sveltejs/acorn-typescript'; +type AdditionalCommentMap = WeakMap; + export { // html Document as HtmlDocument, @@ -41,6 +43,7 @@ export type { // js TsEstree as AstTypes, + AdditionalCommentMap, //css CssChildNode @@ -53,7 +56,7 @@ export type { export function parseScript(content: string): { ast: TsEstree.Program; comments: TsEstree.Comment[]; - additionalComments: WeakMap; + additionalComments: AdditionalCommentMap; } { const comments: TsEstree.Comment[] = []; @@ -96,7 +99,7 @@ export function serializeScript( ast: TsEstree.Node, comments: TsEstree.Comment[], previousContent?: string, - additionalComments?: WeakMap + additionalComments?: AdditionalCommentMap ): string { // @ts-expect-error we are still using `estree` while `esrap` is using `@typescript-eslint/types` // which is causing these errors. But they are simmilar enough to work together. diff --git a/packages/core/tooling/js/common.ts b/packages/core/tooling/js/common.ts index d249b693b..1f51dcf6a 100644 --- a/packages/core/tooling/js/common.ts +++ b/packages/core/tooling/js/common.ts @@ -1,18 +1,30 @@ -import { type AstTypes, Walker, parseScript, serializeScript, stripAst } from '../index.ts'; +import { + type AdditionalCommentMap, + type AstTypes, + Walker, + parseScript, + serializeScript, + stripAst +} from '../index.ts'; import decircular from 'decircular'; import dedent from 'dedent'; -export function addJsDocTypeComment(node: AstTypes.Node, options: { type: string }): void { +export function addJsDocTypeComment( + node: AstTypes.Node, + additionalComments: AdditionalCommentMap, + options: { type: string } +): void { const comment: AstTypes.Comment = { type: 'Block', value: `* @type {${options.type}} ` }; - addComment(node, comment); + addComment(node, additionalComments, comment); } export function addJsDocComment( node: AstTypes.Node, + additionalComments: AdditionalCommentMap, options: { params: Record } ): void { const commentLines: string[] = []; @@ -25,16 +37,23 @@ export function addJsDocComment( value: `*\n * ${commentLines.join('\n * ')}\n ` }; - addComment(node, comment); + addComment(node, additionalComments, comment); } -function addComment(node: AstTypes.Node, comment: AstTypes.Comment) { - node.leadingComments ??= []; - - const found = node.leadingComments.find( - (item) => item.type === 'Block' && item.value === comment.value - ); - if (!found) node.leadingComments.push(comment); +function addComment( + node: AstTypes.Node, + additionalComments: AdditionalCommentMap, + comment: AstTypes.Comment +) { + const found = additionalComments + .get(node) + ?.find((item) => item.type === 'Block' && item.value === comment.value); + + if (!found) { + const comments = additionalComments.get(node) ?? []; + comments.push({ ...comment, position: 'leading' }); + additionalComments.set(node, comments); + } } export function typeAnnotate( diff --git a/packages/core/tooling/js/index.ts b/packages/core/tooling/js/index.ts index 0b4c6e632..206d49d5a 100644 --- a/packages/core/tooling/js/index.ts +++ b/packages/core/tooling/js/index.ts @@ -7,4 +7,4 @@ export * as variables from './variables.ts'; export * as exports from './exports.ts'; export * as kit from './kit.ts'; export * as vite from './vite.ts'; -export type { AstTypes } from '../index.ts'; +export type { AstTypes, AdditionalCommentMap } from '../index.ts'; diff --git a/packages/core/tooling/parsers.ts b/packages/core/tooling/parsers.ts index cd6f3b568..12c2131e5 100644 --- a/packages/core/tooling/parsers.ts +++ b/packages/core/tooling/parsers.ts @@ -1,7 +1,5 @@ import * as utils from './index.ts'; import MagicString from 'magic-string'; -import type { TsEstree } from './js/ts-estree.ts'; -import type { AdditionalComment } from 'esrap/languages/ts'; type ParseBase = { source: string; @@ -11,7 +9,7 @@ type ParseBase = { export function parseScript(source: string): { ast: utils.AstTypes.Program; comments: utils.AstTypes.Comment[]; - additionalComments: WeakMap; + additionalComments: utils.AdditionalCommentMap; } & ParseBase { const { ast, comments, additionalComments } = utils.parseScript(source); const generateCode = () => utils.serializeScript(ast, comments, source, additionalComments); From 6df65fc99717d31837b57e63aaa72f9e8810f48f Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 26 Oct 2025 07:39:04 +0100 Subject: [PATCH 07/24] add changed tests --- packages/core/tests/js/object/create/output.ts | 8 +------- packages/core/tests/js/vite/add-plugin-mode/output.ts | 2 ++ packages/core/tests/js/vite/with-satisfies/output.ts | 11 ++++++----- packages/core/tests/utils.ts | 2 +- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/core/tests/js/object/create/output.ts b/packages/core/tests/js/object/create/output.ts index 28ad96e4f..df190f0f4 100644 --- a/packages/core/tests/js/object/create/output.ts +++ b/packages/core/tests/js/object/create/output.ts @@ -1,15 +1,9 @@ const empty = {}; const created = { foo: 1, bar: 'string' }; -// prettier-ignore const created2 = { foo: 1, bar: 'string', object: { foo: 'hello', nested: { bar: 'world' } }, - array: [ - 123, - 'hello', - { foo: 'bar', bool: true }, - [456, '789'] - ] + array: [123, 'hello', { foo: 'bar', bool: true }, [456, '789']] }; diff --git a/packages/core/tests/js/vite/add-plugin-mode/output.ts b/packages/core/tests/js/vite/add-plugin-mode/output.ts index f112d5fcc..df6d4692a 100644 --- a/packages/core/tests/js/vite/add-plugin-mode/output.ts +++ b/packages/core/tests/js/vite/add-plugin-mode/output.ts @@ -7,8 +7,10 @@ import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ firstPlugin(), + // a default plugin sveltekit(), + middlePlugin(), lastPlugin() ] diff --git a/packages/core/tests/js/vite/with-satisfies/output.ts b/packages/core/tests/js/vite/with-satisfies/output.ts index 4d6ed36e0..9c43348e2 100644 --- a/packages/core/tests/js/vite/with-satisfies/output.ts +++ b/packages/core/tests/js/vite/with-satisfies/output.ts @@ -18,23 +18,24 @@ const config = defineConfig({ plugins: [ // all plugins examples, + tailwindcss(), sveltekit(), kitRoutes(), myPlugin() ], + resolve: { alias: { $lib, $routes, $scripts, $actions } }, - build: { - sourcemap: true, - target: 'esnext', - cssMinify: 'lightningcss' - }, + build: { sourcemap: true, target: 'esnext', cssMinify: 'lightningcss' }, + css: { transformer: 'lightningcss', + lightningcss: { targets: browserslistToTargets(browserslist('defaults, not ie 11')) } }, + experimental: { enableNativePlugin: true } }) satisfies UserConfig; diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index ef3272504..f27f91fc8 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -87,7 +87,7 @@ test('integration - simple', () => { console.log("bar"); const foobar = "foo"; - const foobar2 = "test"; + const foobar2 = 'test'; }" `); }); From ae784550dae249f8b34713c9ebd573f72d52e901 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 26 Oct 2025 07:40:21 +0100 Subject: [PATCH 08/24] thats ok too --- packages/core/tests/js/common/jsdoc-type-comment/output.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/tests/js/common/jsdoc-type-comment/output.ts b/packages/core/tests/js/common/jsdoc-type-comment/output.ts index 1aa734391..e8a715681 100644 --- a/packages/core/tests/js/common/jsdoc-type-comment/output.ts +++ b/packages/core/tests/js/common/jsdoc-type-comment/output.ts @@ -1,2 +1 @@ -/** @type {number} */ -const foo = 42; +/** @type {number} */ const foo = 42; From 1982ac405e60306265e6596fd7273c9632332699 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 26 Oct 2025 07:51:22 +0100 Subject: [PATCH 09/24] those are ok as well --- .../tests/js/object/ensure-nested-property/output.ts | 9 ++++++++- .../core/tests/js/object/override-property/output.ts | 8 +++++++- packages/core/tests/js/object/property-node/output.ts | 7 ++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/core/tests/js/object/ensure-nested-property/output.ts b/packages/core/tests/js/object/ensure-nested-property/output.ts index 08bd5fbf9..b3824d36a 100644 --- a/packages/core/tests/js/object/ensure-nested-property/output.ts +++ b/packages/core/tests/js/object/ensure-nested-property/output.ts @@ -1 +1,8 @@ -const test = { a: { /** a comment */ keep: 'you', b: { c: '007' } } }; +const test = { + a: { + /** a comment */ + keep: 'you', + + b: { c: '007' } + } +}; diff --git a/packages/core/tests/js/object/override-property/output.ts b/packages/core/tests/js/object/override-property/output.ts index fbdef76fe..987c50888 100644 --- a/packages/core/tests/js/object/override-property/output.ts +++ b/packages/core/tests/js/object/override-property/output.ts @@ -1 +1,7 @@ -const test = { /** a comment */ foo: 2, bar: 'string2', lorem: false }; +const test = { + /** a comment */ + foo: 2, + + bar: 'string2', + lorem: false +}; diff --git a/packages/core/tests/js/object/property-node/output.ts b/packages/core/tests/js/object/property-node/output.ts index 98da8dea4..3719acb91 100644 --- a/packages/core/tests/js/object/property-node/output.ts +++ b/packages/core/tests/js/object/property-node/output.ts @@ -1 +1,6 @@ -const test = { /*a comment updated*/ foo: 1, /*aka: bond, james bond*/ james: '007' }; +const test = { + /** a comment */ + foo: 1, + + james: '007' +}; From 62444cc432a419521ff58b48c4dc17eb79751c7a Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 26 Oct 2025 08:28:09 +0100 Subject: [PATCH 10/24] fix last failing test --- packages/core/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 224452035..23c9e2303 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -34,7 +34,7 @@ "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", - "esrap": "https://pkg.pr.new/sveltejs/esrap@718afce", + "esrap": "https://pkg.pr.new/sveltejs/esrap@af12b38", "htmlparser2": "^9.1.0", "magic-string": "^0.30.17", "picocolors": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b189db0f..f8ccbd66c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,8 +167,8 @@ importers: specifier: ^3.2.2 version: 3.2.2 esrap: - specifier: https://pkg.pr.new/sveltejs/esrap@718afce - version: https://pkg.pr.new/sveltejs/esrap@718afce + specifier: https://pkg.pr.new/sveltejs/esrap@af12b38 + version: https://pkg.pr.new/sveltejs/esrap@af12b38 htmlparser2: specifier: ^9.1.0 version: 9.1.0 @@ -1339,8 +1339,8 @@ packages: esrap@1.4.9: resolution: {integrity: sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==} - esrap@https://pkg.pr.new/sveltejs/esrap@718afce: - resolution: {tarball: https://pkg.pr.new/sveltejs/esrap@718afce} + esrap@https://pkg.pr.new/sveltejs/esrap@af12b38: + resolution: {tarball: https://pkg.pr.new/sveltejs/esrap@af12b38} version: 2.1.0 esrecurse@4.3.0: @@ -3402,7 +3402,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - esrap@https://pkg.pr.new/sveltejs/esrap@718afce: + esrap@https://pkg.pr.new/sveltejs/esrap@af12b38: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 From 0b551556a1a0ae486e90d92248210e83f4fd07c3 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Wed, 26 Nov 2025 20:37:22 +0100 Subject: [PATCH 11/24] update esrap --- packages/core/package.json | 2 +- pnpm-lock.yaml | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 4373ab211..e7e70798a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -34,7 +34,7 @@ "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", - "esrap": "https://pkg.pr.new/sveltejs/esrap@af12b38", + "esrap": "^2.2.0", "htmlparser2": "^9.1.0", "magic-string": "^0.30.21", "picocolors": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e7545caa..56936eab8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,8 +167,8 @@ importers: specifier: ^3.2.2 version: 3.2.2 esrap: - specifier: https://pkg.pr.new/sveltejs/esrap@af12b38 - version: https://pkg.pr.new/sveltejs/esrap@af12b38 + specifier: ^2.2.0 + version: 2.2.0 htmlparser2: specifier: ^9.1.0 version: 9.1.0 @@ -1344,9 +1344,8 @@ packages: esrap@2.1.3: resolution: {integrity: sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg==} - esrap@https://pkg.pr.new/sveltejs/esrap@af12b38: - resolution: {tarball: https://pkg.pr.new/sveltejs/esrap@af12b38} - version: 2.1.0 + esrap@2.2.0: + resolution: {integrity: sha512-WBmtxe7R9C5mvL4n2le8nMUe4mD5V9oiK2vJpQ9I3y20ENPUomPcphBXE8D1x/Bm84oN1V+lOfgXxtqmxTp3Xg==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -3403,7 +3402,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - esrap@https://pkg.pr.new/sveltejs/esrap@af12b38: + esrap@2.2.0: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 From 54685d8c51221ff9c5c54ae8cfd38ff9ced17b4f Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 30 Nov 2025 10:38:37 +0100 Subject: [PATCH 12/24] adapt to new api --- packages/addons/eslint/index.ts | 28 +++-- packages/addons/sveltekit-adapter/index.ts | 8 +- .../core/tests/js/common/jsdoc-comment/run.ts | 6 +- .../tests/js/common/jsdoc-type-comment/run.ts | 6 +- packages/core/tests/js/index.ts | 6 +- packages/core/tooling/index.ts | 100 ++++++++++++++---- packages/core/tooling/js/common.ts | 32 +++--- packages/core/tooling/js/index.ts | 2 +- packages/core/tooling/parsers.ts | 11 +- 9 files changed, 128 insertions(+), 71 deletions(-) diff --git a/packages/addons/eslint/index.ts b/packages/addons/eslint/index.ts index 444d4a3e7..1109778da 100644 --- a/packages/addons/eslint/index.ts +++ b/packages/addons/eslint/index.ts @@ -53,7 +53,7 @@ export default defineAddon({ }); sv.file(files.eslintConfig, (content) => { - const { ast, additionalComments, generateCode } = parseScript(content); + const { ast, comments, generateCode } = parseScript(content); const eslintConfigs: Array = []; imports.addDefault(ast, { from: './svelte.config.js', as: 'svelteConfig' }); @@ -85,20 +85,16 @@ export default defineAddon({ if (rules.properties[0].type !== 'Property') { throw new Error('rules.properties[0].type !== "Property"'); } - additionalComments.set(rules.properties[0].key, [ - { - type: 'Line', - value: - ' typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.', - position: 'leading' - }, - { - type: 'Line', - value: - ' see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors', - position: 'leading' - } - ]); + comments.addLeading(rules.properties[0].key, { + type: 'Line', + value: + ' typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.' + }); + comments.addLeading(rules.properties[0].key, { + type: 'Line', + value: + ' see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors' + }); const globalsConfig = object.create({ languageOptions: { @@ -155,7 +151,7 @@ export default defineAddon({ // type annotate config if (!typescript) - common.addJsDocTypeComment(astNode, additionalComments, { + common.addJsDocTypeComment(astNode, comments, { type: "import('eslint').Linter.Config[]" }); diff --git a/packages/addons/sveltekit-adapter/index.ts b/packages/addons/sveltekit-adapter/index.ts index 302129a47..ea27157a0 100644 --- a/packages/addons/sveltekit-adapter/index.ts +++ b/packages/addons/sveltekit-adapter/index.ts @@ -88,7 +88,7 @@ export default defineAddon({ const cfgKitValue = object.property(config, { name: 'kit', fallback }); // removes any existing adapter auto comments - const adapterAutoComments = comments.filter( + const adapterAutoComments = comments.original.filter( (c) => c.loc && cfgKitValue.loc && @@ -96,10 +96,10 @@ export default defineAddon({ c.loc.end.line <= cfgKitValue.loc.end.line ); // modify the array in place - comments.splice( + comments.original.splice( 0, - comments.length, - ...comments.filter((c) => !adapterAutoComments.includes(c)) + comments.original.length, + ...comments.original.filter((c) => !adapterAutoComments.includes(c)) ); } diff --git a/packages/core/tests/js/common/jsdoc-comment/run.ts b/packages/core/tests/js/common/jsdoc-comment/run.ts index 15d0369b9..7b8ec6287 100644 --- a/packages/core/tests/js/common/jsdoc-comment/run.ts +++ b/packages/core/tests/js/common/jsdoc-comment/run.ts @@ -1,9 +1,9 @@ -import { common, type AdditionalCommentMap, type AstTypes } from '@sveltejs/cli-core/js'; +import { common, type Comments, type AstTypes } from '@sveltejs/cli-core/js'; -export function run(ast: AstTypes.Program, additionalComments: AdditionalCommentMap): void { +export function run(ast: AstTypes.Program, comments: Comments): void { const functionDeclaration = ast.body[0] as AstTypes.FunctionDeclaration; - common.addJsDocComment(functionDeclaration, additionalComments, { + common.addJsDocComment(functionDeclaration, comments, { params: { 'import("$lib/paraglide/runtime").AvailableLanguageTag': 'newLanguage' } }); } diff --git a/packages/core/tests/js/common/jsdoc-type-comment/run.ts b/packages/core/tests/js/common/jsdoc-type-comment/run.ts index 5074afd58..129d725a1 100644 --- a/packages/core/tests/js/common/jsdoc-type-comment/run.ts +++ b/packages/core/tests/js/common/jsdoc-type-comment/run.ts @@ -1,13 +1,13 @@ -import { common, variables, type AdditionalCommentMap, type AstTypes } from '@sveltejs/cli-core/js'; +import { common, variables, type Comments, type AstTypes } from '@sveltejs/cli-core/js'; -export function run(ast: AstTypes.Program, additionalComments: AdditionalCommentMap): void { +export function run(ast: AstTypes.Program, comments: Comments): void { const declaration = variables.declaration(ast, { kind: 'const', name: 'foo', value: { type: 'Literal', value: 42 } }); - common.addJsDocTypeComment(declaration, additionalComments, { + common.addJsDocTypeComment(declaration, comments, { type: 'number' }); diff --git a/packages/core/tests/js/index.ts b/packages/core/tests/js/index.ts index bc32a5c83..191aaeac7 100644 --- a/packages/core/tests/js/index.ts +++ b/packages/core/tests/js/index.ts @@ -16,13 +16,13 @@ for (const categoryDirectory of categoryDirectories) { const inputFilePath = join(testDirectoryPath, 'input.ts'); const input = fs.existsSync(inputFilePath) ? fs.readFileSync(inputFilePath, 'utf8') : ''; - const { ast, comments, additionalComments } = parseScript(input); + const { ast, comments } = parseScript(input); // dynamic imports always need to provide the path inline for static analysis const module = await import(`./${categoryDirectory}/${testName}/run.ts`); - module.run(ast, additionalComments); + module.run(ast, comments); - let output = serializeScript(ast, comments, input, additionalComments); + let output = serializeScript(ast, comments, input); if (!output.endsWith('\n')) output += '\n'; await expect(output).toMatchFileSnapshot(`${testDirectoryPath}/output.ts`); }); diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index 9c62d5c7b..e2c23833f 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -14,12 +14,59 @@ import { } from 'postcss'; import * as fleece from 'silver-fleece'; import { print as esrapPrint } from 'esrap'; -import ts, { type AdditionalComment } from 'esrap/languages/ts'; +import ts from 'esrap/languages/ts'; import * as acorn from 'acorn'; import { tsPlugin } from '@sveltejs/acorn-typescript'; import * as yaml from 'yaml'; -type AdditionalCommentMap = WeakMap; +export type CommentType = { type: 'Line' | 'Block'; value: string }; + +export class InternalComments { + comments: Comments; + leading: WeakMap; + trailing: WeakMap; + + constructor() { + this.leading = new WeakMap(); + this.trailing = new WeakMap(); + this.comments = new Comments([], this.leading, this.trailing); + } +} + +/** + * A helper class for managing comments that should be added to AST nodes during code generation. + * Provides methods to add leading comments (before a node) and trailing comments (after a node). + */ +export class Comments { + /** The original comments parsed from source code */ + original: TsEstree.Comment[]; + #leading: WeakMap; + #trailing: WeakMap; + + constructor( + original: TsEstree.Comment[], + leading: WeakMap, + trailing: WeakMap + ) { + this.original = original; + this.#leading = leading; + this.#trailing = trailing; + } + + /** Add a comment that will appear before the given node */ + addLeading(node: TsEstree.Node, comment: CommentType): void { + const list = this.#leading.get(node) ?? []; + list.push(comment); + this.#leading.set(node, list); + } + + /** Add a comment that will appear after the given node */ + addTrailing(node: TsEstree.Node, comment: CommentType): void { + const list = this.#trailing.get(node) ?? []; + list.push(comment); + this.#trailing.set(node, list); + } +} export { // html @@ -44,7 +91,6 @@ export type { // js TsEstree as AstTypes, - AdditionalCommentMap, //css CssChildNode @@ -54,14 +100,15 @@ export type { * Parses as string to an AST. Code below is taken from `esrap` to ensure compatibilty. * https://github.com/sveltejs/esrap/blob/920491535d31484ac5fae2327c7826839d851aed/test/common.js#L14 */ -export function parseScript(content: string): { +export function parseScript( + content: string, + internalComments?: InternalComments +): { ast: TsEstree.Program; - comments: TsEstree.Comment[]; - additionalComments: AdditionalCommentMap; + comments: Comments; } { - const comments: TsEstree.Comment[] = []; - const acornTs = acorn.Parser.extend(tsPlugin()); + internalComments ??= new InternalComments(); const ast = acornTs.parse(content, { ecmaVersion: 'latest', @@ -79,7 +126,7 @@ export function parseScript(content: string): { value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); } - comments.push({ + internalComments.comments.original.push({ type: block ? 'Block' : 'Line', value, start, @@ -91,22 +138,37 @@ export function parseScript(content: string): { return { ast, - comments, - additionalComments: new WeakMap() + comments: internalComments.comments }; } export function serializeScript( ast: TsEstree.Node, - comments: TsEstree.Comment[], - previousContent?: string, - additionalComments?: AdditionalCommentMap + comments: Comments | InternalComments, + previousContent?: string ): string { - // @ts-expect-error we are still using `estree` while `esrap` is using `@typescript-eslint/types` - // which is causing these errors. But they are simmilar enough to work together. - const { code } = esrapPrint(ast, ts({ comments, additionalComments }), { - indent: guessIndentString(previousContent) - }); + const originalComments = 'comments' in comments ? comments.comments.original : comments.original; + const trailingComments = + 'trailing' in comments ? comments.trailing : new WeakMap(); + const leadingComments = + 'leading' in comments ? comments.leading : new WeakMap(); + + const { code } = esrapPrint( + // @ts-expect-error we are still using `estree` while `esrap` is using `@typescript-eslint/types` + // which is causing these errors. But they are simmilar enough to work together. + ast, + ts({ + // @ts-expect-error see above + comments: originalComments?.original, + // @ts-expect-error see above + getLeadingComments: (node) => leadingComments?.get(node), + // @ts-expect-error see above + getTrailingComments: (node) => trailingComments?.get(node) + }), + { + indent: guessIndentString(previousContent) + } + ); return code; } diff --git a/packages/core/tooling/js/common.ts b/packages/core/tooling/js/common.ts index 1f51dcf6a..5af6b8672 100644 --- a/packages/core/tooling/js/common.ts +++ b/packages/core/tooling/js/common.ts @@ -1,6 +1,7 @@ import { - type AdditionalCommentMap, type AstTypes, + type Comments, + InternalComments, Walker, parseScript, serializeScript, @@ -11,7 +12,7 @@ import dedent from 'dedent'; export function addJsDocTypeComment( node: AstTypes.Node, - additionalComments: AdditionalCommentMap, + comments: Comments, options: { type: string } ): void { const comment: AstTypes.Comment = { @@ -19,12 +20,12 @@ export function addJsDocTypeComment( value: `* @type {${options.type}} ` }; - addComment(node, additionalComments, comment); + addComment(node, comments, comment); } export function addJsDocComment( node: AstTypes.Node, - additionalComments: AdditionalCommentMap, + comments: Comments, options: { params: Record } ): void { const commentLines: string[] = []; @@ -37,22 +38,16 @@ export function addJsDocComment( value: `*\n * ${commentLines.join('\n * ')}\n ` }; - addComment(node, additionalComments, comment); + addComment(node, comments, comment); } -function addComment( - node: AstTypes.Node, - additionalComments: AdditionalCommentMap, - comment: AstTypes.Comment -) { - const found = additionalComments - .get(node) - ?.find((item) => item.type === 'Block' && item.value === comment.value); +function addComment(node: AstTypes.Node, comments: Comments, comment: AstTypes.Comment) { + const found = comments.original.find( + (item) => item.type === 'Block' && item.value === comment.value + ); if (!found) { - const comments = additionalComments.get(node) ?? []; - comments.push({ ...comment, position: 'leading' }); - additionalComments.set(node, comments); + comments.addLeading(node, comment); } } @@ -112,7 +107,10 @@ export function areNodesEqual(node: AstTypes.Node, otherNode: AstTypes.Node): bo const nodeClone = stripAst(decircular(node), ['loc', 'raw']); const otherNodeClone = stripAst(decircular(otherNode), ['loc', 'raw']); - return serializeScript(nodeClone, []) === serializeScript(otherNodeClone, []); + return ( + serializeScript(nodeClone, new InternalComments()) === + serializeScript(otherNodeClone, new InternalComments()) + ); } export function createBlockStatement(): AstTypes.BlockStatement { diff --git a/packages/core/tooling/js/index.ts b/packages/core/tooling/js/index.ts index 206d49d5a..21b4faf75 100644 --- a/packages/core/tooling/js/index.ts +++ b/packages/core/tooling/js/index.ts @@ -7,4 +7,4 @@ export * as variables from './variables.ts'; export * as exports from './exports.ts'; export * as kit from './kit.ts'; export * as vite from './vite.ts'; -export type { AstTypes, AdditionalCommentMap } from '../index.ts'; +export type { AstTypes, Comments, CommentType } from '../index.ts'; diff --git a/packages/core/tooling/parsers.ts b/packages/core/tooling/parsers.ts index b578453fd..b0c0242ef 100644 --- a/packages/core/tooling/parsers.ts +++ b/packages/core/tooling/parsers.ts @@ -8,13 +8,14 @@ type ParseBase = { export function parseScript(source: string): { ast: utils.AstTypes.Program; - comments: utils.AstTypes.Comment[]; - additionalComments: utils.AdditionalCommentMap; + comments: utils.Comments; } & ParseBase { - const { ast, comments, additionalComments } = utils.parseScript(source); - const generateCode = () => utils.serializeScript(ast, comments, source, additionalComments); + const internalComments = new utils.InternalComments(); - return { ast, comments, additionalComments, source, generateCode }; + const { ast, comments } = utils.parseScript(source, internalComments); + const generateCode = () => utils.serializeScript(ast, internalComments, source); + + return { ast, comments, source, generateCode }; } export function parseCss(source: string): { ast: utils.CssAst } & ParseBase { From 0e56b5602ca860b186f4aa0ca2fc0e5cb4e521c7 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 30 Nov 2025 10:48:22 +0100 Subject: [PATCH 13/24] improvements --- packages/core/tests/js/index.ts | 7 ++++--- packages/core/tests/utils.ts | 16 ++++++++++------ packages/core/tooling/index.ts | 24 +++++++++--------------- packages/core/tooling/js/common.ts | 6 +----- packages/core/tooling/parsers.ts | 6 +++--- 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/core/tests/js/index.ts b/packages/core/tests/js/index.ts index 191aaeac7..a25dffce0 100644 --- a/packages/core/tests/js/index.ts +++ b/packages/core/tests/js/index.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, test } from 'vitest'; -import { parseScript, serializeScript } from '../../tooling/index.ts'; +import { CommentState, parseScript, serializeScript } from '../../tooling/index.ts'; const baseDir = resolve(fileURLToPath(import.meta.url), '..'); const categoryDirectories = getDirectoryNames(baseDir); @@ -16,13 +16,14 @@ for (const categoryDirectory of categoryDirectories) { const inputFilePath = join(testDirectoryPath, 'input.ts'); const input = fs.existsSync(inputFilePath) ? fs.readFileSync(inputFilePath, 'utf8') : ''; - const { ast, comments } = parseScript(input); + const commentState = new CommentState(); + const { ast, comments } = parseScript(input, commentState); // dynamic imports always need to provide the path inline for static analysis const module = await import(`./${categoryDirectory}/${testName}/run.ts`); module.run(ast, comments); - let output = serializeScript(ast, comments, input); + let output = serializeScript(ast, commentState, input); if (!output.endsWith('\n')) output += '\n'; await expect(output).toMatchFileSnapshot(`${testDirectoryPath}/output.ts`); }); diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index fd0a9783f..f65afd061 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from 'vitest'; import dedent from 'dedent'; import { + CommentState, parseScript, serializeScript, guessIndentString, @@ -76,13 +77,14 @@ test('integration - simple', () => { const foobar = "foo"; } `; - const { ast, comments } = parseScript(code); + const commentState = new CommentState(); + const { ast } = parseScript(code, commentState); const method = ast.body[1] as AstTypes.FunctionDeclaration; method.body.body.push(newVariableDeclaration); // new variable is added with correct indentation and matching quotes - expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(` + expect(serializeScript(ast, commentState, code)).toMatchInlineSnapshot(` "import foo from 'bar'; function bar() { @@ -103,13 +105,14 @@ test('integration - simple 2', () => { const foobar = 'foo'; } `; - const { ast, comments } = parseScript(code); + const commentState = new CommentState(); + const { ast } = parseScript(code, commentState); const method = ast.body[1] as AstTypes.FunctionDeclaration; method.body.body.push(newVariableDeclaration); // new variable is added with correct indentation and matching quotes - expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(` + expect(serializeScript(ast, commentState, code)).toMatchInlineSnapshot(` "import foo from 'bar'; function bar() { @@ -126,9 +129,10 @@ test('integration - preserves comments', () => { /** @type {string} */ let foo = 'bar'; `; - const { ast, comments } = parseScript(code); + const commentState = new CommentState(); + const { ast } = parseScript(code, commentState); - expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(` + expect(serializeScript(ast, commentState, code)).toMatchInlineSnapshot(` "/** @type {string} */ let foo = 'bar';" `); diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index e2c23833f..65ee75cf4 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -21,7 +21,7 @@ import * as yaml from 'yaml'; export type CommentType = { type: 'Line' | 'Block'; value: string }; -export class InternalComments { +export class CommentState { comments: Comments; leading: WeakMap; trailing: WeakMap; @@ -102,13 +102,13 @@ export type { */ export function parseScript( content: string, - internalComments?: InternalComments + commentState?: CommentState ): { ast: TsEstree.Program; comments: Comments; } { const acornTs = acorn.Parser.extend(tsPlugin()); - internalComments ??= new InternalComments(); + commentState ??= new CommentState(); const ast = acornTs.parse(content, { ecmaVersion: 'latest', @@ -126,7 +126,7 @@ export function parseScript( value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); } - internalComments.comments.original.push({ + commentState.comments.original.push({ type: block ? 'Block' : 'Line', value, start, @@ -138,32 +138,26 @@ export function parseScript( return { ast, - comments: internalComments.comments + comments: commentState.comments }; } export function serializeScript( ast: TsEstree.Node, - comments: Comments | InternalComments, + commentState?: CommentState, previousContent?: string ): string { - const originalComments = 'comments' in comments ? comments.comments.original : comments.original; - const trailingComments = - 'trailing' in comments ? comments.trailing : new WeakMap(); - const leadingComments = - 'leading' in comments ? comments.leading : new WeakMap(); - const { code } = esrapPrint( // @ts-expect-error we are still using `estree` while `esrap` is using `@typescript-eslint/types` // which is causing these errors. But they are simmilar enough to work together. ast, ts({ // @ts-expect-error see above - comments: originalComments?.original, + comments: commentState?.comments.original, // @ts-expect-error see above - getLeadingComments: (node) => leadingComments?.get(node), + getLeadingComments: (node) => commentState?.leading.get(node), // @ts-expect-error see above - getTrailingComments: (node) => trailingComments?.get(node) + getTrailingComments: (node) => commentState?.trailing.get(node) }), { indent: guessIndentString(previousContent) diff --git a/packages/core/tooling/js/common.ts b/packages/core/tooling/js/common.ts index 5af6b8672..7f5fb1f01 100644 --- a/packages/core/tooling/js/common.ts +++ b/packages/core/tooling/js/common.ts @@ -1,7 +1,6 @@ import { type AstTypes, type Comments, - InternalComments, Walker, parseScript, serializeScript, @@ -107,10 +106,7 @@ export function areNodesEqual(node: AstTypes.Node, otherNode: AstTypes.Node): bo const nodeClone = stripAst(decircular(node), ['loc', 'raw']); const otherNodeClone = stripAst(decircular(otherNode), ['loc', 'raw']); - return ( - serializeScript(nodeClone, new InternalComments()) === - serializeScript(otherNodeClone, new InternalComments()) - ); + return serializeScript(nodeClone) === serializeScript(otherNodeClone); } export function createBlockStatement(): AstTypes.BlockStatement { diff --git a/packages/core/tooling/parsers.ts b/packages/core/tooling/parsers.ts index b0c0242ef..82fbb0353 100644 --- a/packages/core/tooling/parsers.ts +++ b/packages/core/tooling/parsers.ts @@ -10,10 +10,10 @@ export function parseScript(source: string): { ast: utils.AstTypes.Program; comments: utils.Comments; } & ParseBase { - const internalComments = new utils.InternalComments(); + const state = new utils.CommentState(); - const { ast, comments } = utils.parseScript(source, internalComments); - const generateCode = () => utils.serializeScript(ast, internalComments, source); + const { ast, comments } = utils.parseScript(source, state); + const generateCode = () => utils.serializeScript(ast, state, source); return { ast, comments, source, generateCode }; } From a89a0e90e9f0ce6bbc56d2378d718af0eb17216a Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 30 Nov 2025 10:57:45 +0100 Subject: [PATCH 14/24] cleanup --- packages/core/tests/js/index.ts | 7 +- packages/core/tests/utils.ts | 10 +-- packages/core/tooling/index.ts | 109 +++++++++++++++--------------- packages/core/tooling/js/index.ts | 2 +- packages/core/tooling/parsers.ts | 8 +-- 5 files changed, 63 insertions(+), 73 deletions(-) diff --git a/packages/core/tests/js/index.ts b/packages/core/tests/js/index.ts index a25dffce0..c2abe2e19 100644 --- a/packages/core/tests/js/index.ts +++ b/packages/core/tests/js/index.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, test } from 'vitest'; -import { CommentState, parseScript, serializeScript } from '../../tooling/index.ts'; +import { parseScript, serializeScript } from '../../tooling/index.ts'; const baseDir = resolve(fileURLToPath(import.meta.url), '..'); const categoryDirectories = getDirectoryNames(baseDir); @@ -16,12 +16,11 @@ for (const categoryDirectory of categoryDirectories) { const inputFilePath = join(testDirectoryPath, 'input.ts'); const input = fs.existsSync(inputFilePath) ? fs.readFileSync(inputFilePath, 'utf8') : ''; - const commentState = new CommentState(); - const { ast, comments } = parseScript(input, commentState); + const { ast, commentState } = parseScript(input); // dynamic imports always need to provide the path inline for static analysis const module = await import(`./${categoryDirectory}/${testName}/run.ts`); - module.run(ast, comments); + module.run(ast, commentState.comments); let output = serializeScript(ast, commentState, input); if (!output.endsWith('\n')) output += '\n'; diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index f65afd061..7bdc2bc3b 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from 'vitest'; import dedent from 'dedent'; import { - CommentState, parseScript, serializeScript, guessIndentString, @@ -77,8 +76,7 @@ test('integration - simple', () => { const foobar = "foo"; } `; - const commentState = new CommentState(); - const { ast } = parseScript(code, commentState); + const { ast, commentState } = parseScript(code); const method = ast.body[1] as AstTypes.FunctionDeclaration; method.body.body.push(newVariableDeclaration); @@ -105,8 +103,7 @@ test('integration - simple 2', () => { const foobar = 'foo'; } `; - const commentState = new CommentState(); - const { ast } = parseScript(code, commentState); + const { ast, commentState } = parseScript(code); const method = ast.body[1] as AstTypes.FunctionDeclaration; method.body.body.push(newVariableDeclaration); @@ -129,8 +126,7 @@ test('integration - preserves comments', () => { /** @type {string} */ let foo = 'bar'; `; - const commentState = new CommentState(); - const { ast } = parseScript(code, commentState); + const { ast, commentState } = parseScript(code); expect(serializeScript(ast, commentState, code)).toMatchInlineSnapshot(` "/** @type {string} */ diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index 65ee75cf4..c637cf928 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -19,55 +19,6 @@ import * as acorn from 'acorn'; import { tsPlugin } from '@sveltejs/acorn-typescript'; import * as yaml from 'yaml'; -export type CommentType = { type: 'Line' | 'Block'; value: string }; - -export class CommentState { - comments: Comments; - leading: WeakMap; - trailing: WeakMap; - - constructor() { - this.leading = new WeakMap(); - this.trailing = new WeakMap(); - this.comments = new Comments([], this.leading, this.trailing); - } -} - -/** - * A helper class for managing comments that should be added to AST nodes during code generation. - * Provides methods to add leading comments (before a node) and trailing comments (after a node). - */ -export class Comments { - /** The original comments parsed from source code */ - original: TsEstree.Comment[]; - #leading: WeakMap; - #trailing: WeakMap; - - constructor( - original: TsEstree.Comment[], - leading: WeakMap, - trailing: WeakMap - ) { - this.original = original; - this.#leading = leading; - this.#trailing = trailing; - } - - /** Add a comment that will appear before the given node */ - addLeading(node: TsEstree.Node, comment: CommentType): void { - const list = this.#leading.get(node) ?? []; - list.push(comment); - this.#leading.set(node, list); - } - - /** Add a comment that will appear after the given node */ - addTrailing(node: TsEstree.Node, comment: CommentType): void { - const list = this.#trailing.get(node) ?? []; - list.push(comment); - this.#trailing.set(node, list); - } -} - export { // html Document as HtmlDocument, @@ -100,15 +51,12 @@ export type { * Parses as string to an AST. Code below is taken from `esrap` to ensure compatibilty. * https://github.com/sveltejs/esrap/blob/920491535d31484ac5fae2327c7826839d851aed/test/common.js#L14 */ -export function parseScript( - content: string, - commentState?: CommentState -): { +export function parseScript(content: string): { ast: TsEstree.Program; - comments: Comments; + commentState: CommentState; } { const acornTs = acorn.Parser.extend(tsPlugin()); - commentState ??= new CommentState(); + const commentState = new CommentState(); const ast = acornTs.parse(content, { ecmaVersion: 'latest', @@ -138,7 +86,7 @@ export function parseScript( return { ast, - comments: commentState.comments + commentState }; } @@ -272,3 +220,52 @@ export function parseYaml(content: string): ReturnType): string { return yaml.stringify(data, { singleQuote: true }); } + +export type CommentType = { type: 'Line' | 'Block'; value: string }; + +export class CommentState { + comments: Comments; + leading: WeakMap; + trailing: WeakMap; + + constructor() { + this.leading = new WeakMap(); + this.trailing = new WeakMap(); + this.comments = new Comments([], this.leading, this.trailing); + } +} + +/** + * A helper class for managing comments that should be added to AST nodes during code generation. + * Provides methods to add leading comments (before a node) and trailing comments (after a node). + */ +export class Comments { + /** The original comments parsed from source code */ + original: TsEstree.Comment[]; + #leading: WeakMap; + #trailing: WeakMap; + + constructor( + original: TsEstree.Comment[], + leading: WeakMap, + trailing: WeakMap + ) { + this.original = original; + this.#leading = leading; + this.#trailing = trailing; + } + + /** Add a comment that will appear before the given node */ + addLeading(node: TsEstree.Node, comment: CommentType): void { + const list = this.#leading.get(node) ?? []; + list.push(comment); + this.#leading.set(node, list); + } + + /** Add a comment that will appear after the given node */ + addTrailing(node: TsEstree.Node, comment: CommentType): void { + const list = this.#trailing.get(node) ?? []; + list.push(comment); + this.#trailing.set(node, list); + } +} diff --git a/packages/core/tooling/js/index.ts b/packages/core/tooling/js/index.ts index 21b4faf75..c53336b61 100644 --- a/packages/core/tooling/js/index.ts +++ b/packages/core/tooling/js/index.ts @@ -7,4 +7,4 @@ export * as variables from './variables.ts'; export * as exports from './exports.ts'; export * as kit from './kit.ts'; export * as vite from './vite.ts'; -export type { AstTypes, Comments, CommentType } from '../index.ts'; +export type { AstTypes, Comments } from '../index.ts'; diff --git a/packages/core/tooling/parsers.ts b/packages/core/tooling/parsers.ts index 82fbb0353..bb466f60a 100644 --- a/packages/core/tooling/parsers.ts +++ b/packages/core/tooling/parsers.ts @@ -10,12 +10,10 @@ export function parseScript(source: string): { ast: utils.AstTypes.Program; comments: utils.Comments; } & ParseBase { - const state = new utils.CommentState(); + const { ast, commentState } = utils.parseScript(source); + const generateCode = () => utils.serializeScript(ast, commentState, source); - const { ast, comments } = utils.parseScript(source, state); - const generateCode = () => utils.serializeScript(ast, state, source); - - return { ast, comments, source, generateCode }; + return { ast, comments: commentState.comments, source, generateCode }; } export function parseCss(source: string): { ast: utils.CssAst } & ParseBase { From 4efbc25189347fedf2fa9f116d29165a21464ca5 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 30 Nov 2025 11:04:22 +0100 Subject: [PATCH 15/24] remove useless comment --- packages/core/tooling/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index c637cf928..4fc010460 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -235,10 +235,6 @@ export class CommentState { } } -/** - * A helper class for managing comments that should be added to AST nodes during code generation. - * Provides methods to add leading comments (before a node) and trailing comments (after a node). - */ export class Comments { /** The original comments parsed from source code */ original: TsEstree.Comment[]; From 15d3400b7d4bb2104046bf4a8cb970bd802b1e61 Mon Sep 17 00:00:00 2001 From: Manuel <30698007+manuel3108@users.noreply.github.com> Date: Sun, 30 Nov 2025 11:10:06 +0100 Subject: [PATCH 16/24] Update packages/core/tooling/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/core/tooling/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index c637cf928..1cf64801f 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -97,7 +97,7 @@ export function serializeScript( ): string { const { code } = esrapPrint( // @ts-expect-error we are still using `estree` while `esrap` is using `@typescript-eslint/types` - // which is causing these errors. But they are simmilar enough to work together. + // which is causing these errors. But they are similar enough to work together. ast, ts({ // @ts-expect-error see above From 7160300c3681295cbe126d88482bda78311d7ab9 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Mon, 1 Dec 2025 19:46:19 +0100 Subject: [PATCH 17/24] re-add quote style --- packages/core/tests/utils.ts | 56 ++++++++++++++++++++++++++++++++-- packages/core/tooling/index.ts | 36 +++++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index 7bdc2bc3b..63034b751 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -6,7 +6,8 @@ import { guessIndentString, type AstTypes, serializeYaml, - parseYaml + parseYaml, + guessQuoteStyle } from '../tooling/index.ts'; test('guessIndentString - one tab', () => { @@ -49,6 +50,57 @@ test('guessIndentString - eight spaces', () => { expect(guessIndentString(code)).toBe(' '); }); +test('guessQuoteStyle - single simple', () => { + const code = dedent` + console.log('asd'); + `; + const { ast } = parseScript(code); + + expect(guessQuoteStyle(ast)).toBe('single'); +}); + +test('guessQuoteStyle - single complex', () => { + const code = dedent` + import foo from 'bar'; + + console.log("bar"); + const foobar = 'foo'; + `; + const { ast } = parseScript(code); + + expect(guessQuoteStyle(ast)).toBe('single'); +}); + +test('guessQuoteStyle - double simple', () => { + const code = dedent` + console.log("asd"); + `; + const { ast } = parseScript(code); + + expect(guessQuoteStyle(ast)).toBe('double'); +}); + +test('guessQuoteStyle - double complex', () => { + const code = dedent` + import foo from 'bar'; + + console.log("bar"); + const foobar = "foo"; + `; + const { ast } = parseScript(code); + + expect(guessQuoteStyle(ast)).toBe('double'); +}); + +test('guessQuoteStyle - no quotes', () => { + const code = dedent` + const foo = true; + `; + const { ast } = parseScript(code); + + expect(guessQuoteStyle(ast)).toBe(undefined); +}); + const newVariableDeclaration: AstTypes.VariableDeclaration = { type: 'VariableDeclaration', kind: 'const', @@ -89,7 +141,7 @@ test('integration - simple', () => { console.log("bar"); const foobar = "foo"; - const foobar2 = 'test'; + const foobar2 = "test"; }" `); }); diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index 46822e26e..33b2f9413 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -105,7 +105,8 @@ export function serializeScript( // @ts-expect-error see above getLeadingComments: (node) => commentState?.leading.get(node), // @ts-expect-error see above - getTrailingComments: (node) => commentState?.trailing.get(node) + getTrailingComments: (node) => commentState?.trailing.get(node), + quotes: guessQuoteStyle(ast) }), { indent: guessIndentString(previousContent) @@ -213,6 +214,39 @@ export function guessIndentString(str: string | undefined): string { } } +export function guessQuoteStyle(ast: TsEstree.Node): 'single' | 'double' | undefined { + let singleCount = 0; + let doubleCount = 0; + + Walker.walk(ast, null, { + Literal(node) { + if (node.raw && node.raw.length >= 2) { + // we have at least two characters in the raw string that could represent both quotes + const quotes = [node.raw[0], node.raw[node.raw.length - 1]]; + for (const quote of quotes) { + switch (quote) { + case "'": + singleCount++; + break; + case '"': + doubleCount++; + break; + default: + break; + } + } + } + } + }); + + if (singleCount === 0 && doubleCount === 0) { + // new file or file without any quotes + return undefined; + } + + return singleCount > doubleCount ? 'single' : 'double'; +} + export function parseYaml(content: string): ReturnType { return yaml.parseDocument(content); } From 748cc4ece4e19e6b5f545f92616941f55cbf9763 Mon Sep 17 00:00:00 2001 From: jycouet Date: Mon, 1 Dec 2025 22:02:51 +0100 Subject: [PATCH 18/24] using esrap 2.2.1 --- packages/core/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index e7e70798a..7019a1447 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -34,7 +34,7 @@ "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", - "esrap": "^2.2.0", + "esrap": "^2.2.1", "htmlparser2": "^9.1.0", "magic-string": "^0.30.21", "picocolors": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56936eab8..715466c8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,8 +167,8 @@ importers: specifier: ^3.2.2 version: 3.2.2 esrap: - specifier: ^2.2.0 - version: 2.2.0 + specifier: ^2.2.1 + version: 2.2.1 htmlparser2: specifier: ^9.1.0 version: 9.1.0 @@ -1344,8 +1344,8 @@ packages: esrap@2.1.3: resolution: {integrity: sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg==} - esrap@2.2.0: - resolution: {integrity: sha512-WBmtxe7R9C5mvL4n2le8nMUe4mD5V9oiK2vJpQ9I3y20ENPUomPcphBXE8D1x/Bm84oN1V+lOfgXxtqmxTp3Xg==} + esrap@2.2.1: + resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -3402,7 +3402,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - esrap@2.2.0: + esrap@2.2.1: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 From d7ff4b2d00ec64ca0bef9cb019348ab941873b5d Mon Sep 17 00:00:00 2001 From: jycouet Date: Mon, 1 Dec 2025 22:37:24 +0100 Subject: [PATCH 19/24] simplifying? --- packages/core/tests/js/index.ts | 6 ++-- packages/core/tests/utils.ts | 12 ++++---- packages/core/tooling/index.ts | 51 ++++++++++---------------------- packages/core/tooling/parsers.ts | 6 ++-- 4 files changed, 28 insertions(+), 47 deletions(-) diff --git a/packages/core/tests/js/index.ts b/packages/core/tests/js/index.ts index c2abe2e19..191aaeac7 100644 --- a/packages/core/tests/js/index.ts +++ b/packages/core/tests/js/index.ts @@ -16,13 +16,13 @@ for (const categoryDirectory of categoryDirectories) { const inputFilePath = join(testDirectoryPath, 'input.ts'); const input = fs.existsSync(inputFilePath) ? fs.readFileSync(inputFilePath, 'utf8') : ''; - const { ast, commentState } = parseScript(input); + const { ast, comments } = parseScript(input); // dynamic imports always need to provide the path inline for static analysis const module = await import(`./${categoryDirectory}/${testName}/run.ts`); - module.run(ast, commentState.comments); + module.run(ast, comments); - let output = serializeScript(ast, commentState, input); + let output = serializeScript(ast, comments, input); if (!output.endsWith('\n')) output += '\n'; await expect(output).toMatchFileSnapshot(`${testDirectoryPath}/output.ts`); }); diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index 63034b751..5abb3bd1c 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -128,13 +128,13 @@ test('integration - simple', () => { const foobar = "foo"; } `; - const { ast, commentState } = parseScript(code); + const { ast, comments } = parseScript(code); const method = ast.body[1] as AstTypes.FunctionDeclaration; method.body.body.push(newVariableDeclaration); // new variable is added with correct indentation and matching quotes - expect(serializeScript(ast, commentState, code)).toMatchInlineSnapshot(` + expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(` "import foo from 'bar'; function bar() { @@ -155,13 +155,13 @@ test('integration - simple 2', () => { const foobar = 'foo'; } `; - const { ast, commentState } = parseScript(code); + const { ast, comments } = parseScript(code); const method = ast.body[1] as AstTypes.FunctionDeclaration; method.body.body.push(newVariableDeclaration); // new variable is added with correct indentation and matching quotes - expect(serializeScript(ast, commentState, code)).toMatchInlineSnapshot(` + expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(` "import foo from 'bar'; function bar() { @@ -178,9 +178,9 @@ test('integration - preserves comments', () => { /** @type {string} */ let foo = 'bar'; `; - const { ast, commentState } = parseScript(code); + const { ast, comments } = parseScript(code); - expect(serializeScript(ast, commentState, code)).toMatchInlineSnapshot(` + expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(` "/** @type {string} */ let foo = 'bar';" `); diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index 33b2f9413..42641240d 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -53,10 +53,10 @@ export type { */ export function parseScript(content: string): { ast: TsEstree.Program; - commentState: CommentState; + comments: Comments; } { const acornTs = acorn.Parser.extend(tsPlugin()); - const commentState = new CommentState(); + const comments = new Comments(); const ast = acornTs.parse(content, { ecmaVersion: 'latest', @@ -74,7 +74,7 @@ export function parseScript(content: string): { value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); } - commentState.comments.original.push({ + comments.original.push({ type: block ? 'Block' : 'Line', value, start, @@ -84,15 +84,12 @@ export function parseScript(content: string): { } }) as TsEstree.Program; - return { - ast, - commentState - }; + return { ast, comments }; } export function serializeScript( ast: TsEstree.Node, - commentState?: CommentState, + comments?: Comments, previousContent?: string ): string { const { code } = esrapPrint( @@ -101,11 +98,11 @@ export function serializeScript( ast, ts({ // @ts-expect-error see above - comments: commentState?.comments.original, + comments: comments?.original, // @ts-expect-error see above - getLeadingComments: (node) => commentState?.leading.get(node), + getLeadingComments: (node) => comments?.leading.get(node), // @ts-expect-error see above - getTrailingComments: (node) => commentState?.trailing.get(node), + getTrailingComments: (node) => comments?.trailing.get(node), quotes: guessQuoteStyle(ast) }), { @@ -257,45 +254,29 @@ export function serializeYaml(data: ReturnType): stri export type CommentType = { type: 'Line' | 'Block'; value: string }; -export class CommentState { - comments: Comments; +export class Comments { + /** The original comments parsed from source code */ + original: TsEstree.Comment[]; leading: WeakMap; trailing: WeakMap; constructor() { + this.original = []; this.leading = new WeakMap(); this.trailing = new WeakMap(); - this.comments = new Comments([], this.leading, this.trailing); - } -} - -export class Comments { - /** The original comments parsed from source code */ - original: TsEstree.Comment[]; - #leading: WeakMap; - #trailing: WeakMap; - - constructor( - original: TsEstree.Comment[], - leading: WeakMap, - trailing: WeakMap - ) { - this.original = original; - this.#leading = leading; - this.#trailing = trailing; } /** Add a comment that will appear before the given node */ addLeading(node: TsEstree.Node, comment: CommentType): void { - const list = this.#leading.get(node) ?? []; + const list = this.leading.get(node) ?? []; list.push(comment); - this.#leading.set(node, list); + this.leading.set(node, list); } /** Add a comment that will appear after the given node */ addTrailing(node: TsEstree.Node, comment: CommentType): void { - const list = this.#trailing.get(node) ?? []; + const list = this.trailing.get(node) ?? []; list.push(comment); - this.#trailing.set(node, list); + this.trailing.set(node, list); } } diff --git a/packages/core/tooling/parsers.ts b/packages/core/tooling/parsers.ts index bb466f60a..94b913fed 100644 --- a/packages/core/tooling/parsers.ts +++ b/packages/core/tooling/parsers.ts @@ -10,10 +10,10 @@ export function parseScript(source: string): { ast: utils.AstTypes.Program; comments: utils.Comments; } & ParseBase { - const { ast, commentState } = utils.parseScript(source); - const generateCode = () => utils.serializeScript(ast, commentState, source); + const { ast, comments } = utils.parseScript(source); + const generateCode = () => utils.serializeScript(ast, comments, source); - return { ast, comments: commentState.comments, source, generateCode }; + return { ast, comments, source, generateCode }; } export function parseCss(source: string): { ast: utils.CssAst } & ParseBase { From 9ef38e36772aeaa329b6f06256a91e75c76a70d4 Mon Sep 17 00:00:00 2001 From: jycouet Date: Mon, 1 Dec 2025 23:01:02 +0100 Subject: [PATCH 20/24] good enough? --- packages/core/tooling/index.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index 42641240d..9d6e8326a 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -18,6 +18,7 @@ import ts from 'esrap/languages/ts'; import * as acorn from 'acorn'; import { tsPlugin } from '@sveltejs/acorn-typescript'; import * as yaml from 'yaml'; +import type { BaseNode } from 'estree'; export { // html @@ -99,9 +100,7 @@ export function serializeScript( ts({ // @ts-expect-error see above comments: comments?.original, - // @ts-expect-error see above getLeadingComments: (node) => comments?.leading.get(node), - // @ts-expect-error see above getTrailingComments: (node) => comments?.trailing.get(node), quotes: guessQuoteStyle(ast) }), @@ -257,8 +256,8 @@ export type CommentType = { type: 'Line' | 'Block'; value: string }; export class Comments { /** The original comments parsed from source code */ original: TsEstree.Comment[]; - leading: WeakMap; - trailing: WeakMap; + leading: WeakMap; + trailing: WeakMap; constructor() { this.original = []; @@ -267,14 +266,14 @@ export class Comments { } /** Add a comment that will appear before the given node */ - addLeading(node: TsEstree.Node, comment: CommentType): void { + addLeading(node: BaseNode, comment: CommentType): void { const list = this.leading.get(node) ?? []; list.push(comment); this.leading.set(node, list); } /** Add a comment that will appear after the given node */ - addTrailing(node: TsEstree.Node, comment: CommentType): void { + addTrailing(node: BaseNode, comment: CommentType): void { const list = this.trailing.get(node) ?? []; list.push(comment); this.trailing.set(node, list); From 2e6b85d098c21502402e9cefbe04c738c1714055 Mon Sep 17 00:00:00 2001 From: jycouet Date: Tue, 2 Dec 2025 23:16:43 +0100 Subject: [PATCH 21/24] simplify --- packages/core/tests/js/common/jsdoc-comment/run.ts | 5 +++++ packages/core/tooling/js/common.ts | 14 ++------------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/core/tests/js/common/jsdoc-comment/run.ts b/packages/core/tests/js/common/jsdoc-comment/run.ts index 7b8ec6287..9ed0335eb 100644 --- a/packages/core/tests/js/common/jsdoc-comment/run.ts +++ b/packages/core/tests/js/common/jsdoc-comment/run.ts @@ -6,4 +6,9 @@ export function run(ast: AstTypes.Program, comments: Comments): void { common.addJsDocComment(functionDeclaration, comments, { params: { 'import("$lib/paraglide/runtime").AvailableLanguageTag': 'newLanguage' } }); + + // Adding 2 times the same comment should not add it twice! + common.addJsDocComment(functionDeclaration, comments, { + params: { 'import("$lib/paraglide/runtime").AvailableLanguageTag': 'newLanguage' } + }); } diff --git a/packages/core/tooling/js/common.ts b/packages/core/tooling/js/common.ts index 7f5fb1f01..b1e2c7c8a 100644 --- a/packages/core/tooling/js/common.ts +++ b/packages/core/tooling/js/common.ts @@ -19,7 +19,7 @@ export function addJsDocTypeComment( value: `* @type {${options.type}} ` }; - addComment(node, comments, comment); + comments.add(node, comment); } export function addJsDocComment( @@ -37,17 +37,7 @@ export function addJsDocComment( value: `*\n * ${commentLines.join('\n * ')}\n ` }; - addComment(node, comments, comment); -} - -function addComment(node: AstTypes.Node, comments: Comments, comment: AstTypes.Comment) { - const found = comments.original.find( - (item) => item.type === 'Block' && item.value === comment.value - ); - - if (!found) { - comments.addLeading(node, comment); - } + comments.add(node, comment); } export function typeAnnotate( From 80533146bb9c4befde2c4489bfebe43ae548e432 Mon Sep 17 00:00:00 2001 From: jycouet Date: Tue, 2 Dec 2025 23:25:00 +0100 Subject: [PATCH 22/24] add & remove --- packages/addons/eslint/index.ts | 4 +- packages/addons/sveltekit-adapter/index.ts | 8 +--- packages/core/tests/utils.ts | 21 +++++++++ packages/core/tooling/index.ts | 54 ++++++++++++++-------- 4 files changed, 60 insertions(+), 27 deletions(-) diff --git a/packages/addons/eslint/index.ts b/packages/addons/eslint/index.ts index 1109778da..2a671fc54 100644 --- a/packages/addons/eslint/index.ts +++ b/packages/addons/eslint/index.ts @@ -85,12 +85,12 @@ export default defineAddon({ if (rules.properties[0].type !== 'Property') { throw new Error('rules.properties[0].type !== "Property"'); } - comments.addLeading(rules.properties[0].key, { + comments.add(rules.properties[0].key, { type: 'Line', value: ' typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.' }); - comments.addLeading(rules.properties[0].key, { + comments.add(rules.properties[0].key, { type: 'Line', value: ' see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors' diff --git a/packages/addons/sveltekit-adapter/index.ts b/packages/addons/sveltekit-adapter/index.ts index ea27157a0..3806668c4 100644 --- a/packages/addons/sveltekit-adapter/index.ts +++ b/packages/addons/sveltekit-adapter/index.ts @@ -88,19 +88,13 @@ export default defineAddon({ const cfgKitValue = object.property(config, { name: 'kit', fallback }); // removes any existing adapter auto comments - const adapterAutoComments = comments.original.filter( + comments.remove( (c) => c.loc && cfgKitValue.loc && c.loc.start.line >= cfgKitValue.loc.start.line && c.loc.end.line <= cfgKitValue.loc.end.line ); - // modify the array in place - comments.original.splice( - 0, - comments.original.length, - ...comments.original.filter((c) => !adapterAutoComments.includes(c)) - ); } return generateCode(); diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index 5abb3bd1c..585c6fcaa 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -186,6 +186,27 @@ test('integration - preserves comments', () => { `); }); +test('integration - removes comments', () => { + const code = dedent` + let foo = { + /** @type {string} */ + bar: 'baz', + /** @type {number} */ + baz: 1, + }; + `; + const { ast, comments } = parseScript(code); + comments.remove((c) => c.value.includes('number')); + expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(` + "let foo = { + /** @type {string} */ + bar: 'baz', + + baz: 1 + };" + `); +}); + describe('yaml', () => { test('read and write', () => { const input = dedent`foo: diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index 9d6e8326a..e5668e610 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -75,7 +75,7 @@ export function parseScript(content: string): { value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); } - comments.original.push({ + comments.addOriginal({ type: block ? 'Block' : 'Line', value, start, @@ -99,9 +99,9 @@ export function serializeScript( ast, ts({ // @ts-expect-error see above - comments: comments?.original, - getLeadingComments: (node) => comments?.leading.get(node), - getTrailingComments: (node) => comments?.trailing.get(node), + comments: comments?.getOriginal(), + getLeadingComments: (node) => comments?.getLeading(node), + getTrailingComments: (node) => comments?.getTrailing(node), quotes: guessQuoteStyle(ast) }), { @@ -254,10 +254,9 @@ export function serializeYaml(data: ReturnType): stri export type CommentType = { type: 'Line' | 'Block'; value: string }; export class Comments { - /** The original comments parsed from source code */ - original: TsEstree.Comment[]; - leading: WeakMap; - trailing: WeakMap; + private original: TsEstree.Comment[]; + private leading: WeakMap; + private trailing: WeakMap; constructor() { this.original = []; @@ -265,17 +264,36 @@ export class Comments { this.trailing = new WeakMap(); } - /** Add a comment that will appear before the given node */ - addLeading(node: BaseNode, comment: CommentType): void { - const list = this.leading.get(node) ?? []; - list.push(comment); - this.leading.set(node, list); + addOriginal(comment: TsEstree.Comment): void { + this.original.push(comment); } - /** Add a comment that will appear after the given node */ - addTrailing(node: BaseNode, comment: CommentType): void { - const list = this.trailing.get(node) ?? []; - list.push(comment); - this.trailing.set(node, list); + getOriginal(): TsEstree.Comment[] { + return this.original; + } + + getLeading(node: BaseNode): CommentType[] | undefined { + return this.leading.get(node); + } + + getTrailing(node: BaseNode): CommentType[] | undefined { + return this.trailing.get(node); + } + + add(node: BaseNode, comment: CommentType, options?: { position?: 'leading' | 'trailing' }): void { + const { position = 'leading' } = options ?? {}; + const map = position === 'leading' ? this.leading : this.trailing; + const list = map.get(node) ?? []; + // Let's not add 2 times the same comment to one node! + if (!list.find((c) => c.value === comment.value)) { + list.push(comment); + map.set(node, list); + } + } + + remove(predicate: (comment: TsEstree.Comment) => boolean | undefined | null): void { + const filtered = this.original.filter((c) => !predicate(c)); + this.original.length = 0; + this.original.push(...filtered); } } From 44d7a85cc10ea9f410e0cdf68a0b21d6b88282d5 Mon Sep 17 00:00:00 2001 From: jycouet Date: Tue, 2 Dec 2025 23:42:23 +0100 Subject: [PATCH 23/24] api? --- packages/core/tooling/index.ts | 40 +++++++++++++++------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts index e5668e610..c9fcc7074 100644 --- a/packages/core/tooling/index.ts +++ b/packages/core/tooling/index.ts @@ -58,6 +58,7 @@ export function parseScript(content: string): { } { const acornTs = acorn.Parser.extend(tsPlugin()); const comments = new Comments(); + const internal = transformToInternal(comments); const ast = acornTs.parse(content, { ecmaVersion: 'latest', @@ -75,7 +76,7 @@ export function parseScript(content: string): { value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); } - comments.addOriginal({ + internal.original.push({ type: block ? 'Block' : 'Line', value, start, @@ -93,15 +94,16 @@ export function serializeScript( comments?: Comments, previousContent?: string ): string { + const internal = transformToInternal(comments); const { code } = esrapPrint( // @ts-expect-error we are still using `estree` while `esrap` is using `@typescript-eslint/types` // which is causing these errors. But they are similar enough to work together. ast, ts({ // @ts-expect-error see above - comments: comments?.getOriginal(), - getLeadingComments: (node) => comments?.getLeading(node), - getTrailingComments: (node) => comments?.getTrailing(node), + comments: internal.original, + getLeadingComments: (node) => internal.leading.get(node), + getTrailingComments: (node) => internal.trailing.get(node), quotes: guessQuoteStyle(ast) }), { @@ -264,22 +266,6 @@ export class Comments { this.trailing = new WeakMap(); } - addOriginal(comment: TsEstree.Comment): void { - this.original.push(comment); - } - - getOriginal(): TsEstree.Comment[] { - return this.original; - } - - getLeading(node: BaseNode): CommentType[] | undefined { - return this.leading.get(node); - } - - getTrailing(node: BaseNode): CommentType[] | undefined { - return this.trailing.get(node); - } - add(node: BaseNode, comment: CommentType, options?: { position?: 'leading' | 'trailing' }): void { const { position = 'leading' } = options ?? {}; const map = position === 'leading' ? this.leading : this.trailing; @@ -292,8 +278,16 @@ export class Comments { } remove(predicate: (comment: TsEstree.Comment) => boolean | undefined | null): void { - const filtered = this.original.filter((c) => !predicate(c)); - this.original.length = 0; - this.original.push(...filtered); + this.original = this.original.filter((c) => !predicate(c)); } } + +interface CommentsInternal { + original: TsEstree.Comment[]; + leading: WeakMap; + trailing: WeakMap; +} + +function transformToInternal(comments: Comments | undefined): CommentsInternal { + return (comments ?? new Comments()) as unknown as CommentsInternal; +} From 3afb16119cc4a902ce70e1d30236a4f594cbe7ea Mon Sep 17 00:00:00 2001 From: jycouet Date: Tue, 2 Dec 2025 23:44:49 +0100 Subject: [PATCH 24/24] manual snapshot review OK --- .../create-with-all-addons/playwright.config.ts | 5 +---- .../snapshots/create-with-all-addons/src/app.d.ts | 13 ++++++++----- .../create-with-all-addons/src/hooks.server.ts | 2 ++ .../create-with-all-addons/svelte.config.js | 1 + .../snapshots/create-with-all-addons/vite.config.ts | 12 ++++++++---- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/cli/tests/snapshots/create-with-all-addons/playwright.config.ts b/packages/cli/tests/snapshots/create-with-all-addons/playwright.config.ts index f6c81af8a..8f5062c2f 100644 --- a/packages/cli/tests/snapshots/create-with-all-addons/playwright.config.ts +++ b/packages/cli/tests/snapshots/create-with-all-addons/playwright.config.ts @@ -1,9 +1,6 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ - webServer: { - command: 'npm run build && npm run preview', - port: 4173 - }, + webServer: { command: 'npm run build && npm run preview', port: 4173 }, testDir: 'e2e' }); diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/app.d.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/app.d.ts index a6c98c5eb..bb1c423bb 100644 --- a/packages/cli/tests/snapshots/create-with-all-addons/src/app.d.ts +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/app.d.ts @@ -6,10 +6,13 @@ declare global { user: import('$lib/server/auth').SessionValidationResult['user']; session: import('$lib/server/auth').SessionValidationResult['session'] } - } // interface Error {} - // interface Locals {} -} // interface PageData {} -// interface PageState {} -// interface Platform {} + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + export {}; diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts index 48874a52b..2fab5d931 100644 --- a/packages/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts @@ -17,6 +17,7 @@ const handleAuth: Handle = async ({ event, resolve }) => { if (!sessionToken) { event.locals.user = null; event.locals.session = null; + return resolve(event); } @@ -30,6 +31,7 @@ const handleAuth: Handle = async ({ event, resolve }) => { event.locals.user = user; event.locals.session = session; + return resolve(event); }; diff --git a/packages/cli/tests/snapshots/create-with-all-addons/svelte.config.js b/packages/cli/tests/snapshots/create-with-all-addons/svelte.config.js index ad0e153f2..f59ea15cb 100644 --- a/packages/cli/tests/snapshots/create-with-all-addons/svelte.config.js +++ b/packages/cli/tests/snapshots/create-with-all-addons/svelte.config.js @@ -7,6 +7,7 @@ const config = { // Consult https://svelte.dev/docs/kit/integrations // for more information about preprocessors preprocess: [vitePreprocess(), mdsvex()], + kit: { adapter: adapter() }, extensions: ['.svelte', '.svx'] }; diff --git a/packages/cli/tests/snapshots/create-with-all-addons/vite.config.ts b/packages/cli/tests/snapshots/create-with-all-addons/vite.config.ts index 9439a7058..6f612e940 100644 --- a/packages/cli/tests/snapshots/create-with-all-addons/vite.config.ts +++ b/packages/cli/tests/snapshots/create-with-all-addons/vite.config.ts @@ -10,29 +10,33 @@ export default defineConfig({ tailwindcss(), sveltekit(), devtoolsJson(), - paraglideVitePlugin({ - project: './project.inlang', - outdir: './src/lib/paraglide' - }) + paraglideVitePlugin({ project: './project.inlang', outdir: './src/lib/paraglide' }) ], + test: { expect: { requireAssertions: true }, + projects: [ { extends: './vite.config.ts', + test: { name: 'client', + browser: { enabled: true, provider: playwright(), instances: [{ browser: 'chromium', headless: true }] }, + include: ['src/**/*.svelte.{test,spec}.{js,ts}'], exclude: ['src/lib/server/**'] } }, + { extends: './vite.config.ts', + test: { name: 'server', environment: 'node',