diff --git a/packages/core/package.json b/packages/core/package.json index 8897e990..cc4707e7 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 c7f2021e..9aca4de6 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 37979d0b..ef327250 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 ac42aa48..450666ed 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 8812660e..d249b693 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 f7764d34..7d39bad2 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 959d7df3..0f359645 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