From ada8b2e17cfc54c659cd873b5302400e2fe8554d Mon Sep 17 00:00:00 2001 From: sooniter Date: Mon, 28 Sep 2026 19:17:32 +0800 Subject: [PATCH 1/7] refactor(fmt): simplify SWC Next postprocessing --- packages/rstack/src/fmt/swcNextPlugin.ts | 47 ++++++++----------- .../rstack/tests/fmt/swcNextPlugin.test.ts | 9 ++++ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/rstack/src/fmt/swcNextPlugin.ts b/packages/rstack/src/fmt/swcNextPlugin.ts index e8e335d..4e7d366 100644 --- a/packages/rstack/src/fmt/swcNextPlugin.ts +++ b/packages/rstack/src/fmt/swcNextPlugin.ts @@ -1,5 +1,10 @@ -// Adapted from @prettier/plugin-yuku to use SWC Next: -// https://github.com/prettier/prettier/tree/main/packages/plugin-yuku +// Differences from @prettier/plugin-yuku at the original port's upstream revision: +// https://github.com/prettier/prettier/blob/315f28198200d7678dadd3fd5eece499b127ff2a/packages/plugin-yuku/index.js +// https://github.com/prettier/prettier/blob/315f28198200d7678dadd3fd5eece499b127ff2a/src/language-js/parse/postprocess/index.js +// - Reuses the ESTree printer and keeps only SWC Next's JS/TS normalization. +// - Masks comments in one pass and uses binary search for type-cast comments. +// - Omits Hack pipelines (unsupported) and template-shape checks (guaranteed by SWC Next). +// - Adds babel/typescript parser aliases, with a Babel File root for Vue. import * as prettierEstreePlugin from 'prettier/plugins/estree'; import type { Parser, ParserOptions, Plugin } from 'prettier'; @@ -305,7 +310,8 @@ const visitNode = (value: unknown, options: VisitOptions): unknown => { return value; } - let node = asAstNode(value); + // Trust the parser's child nodes; validate the root at the parser entry point. + let node = value as AstNode; if (options.onEnter) { const result = options.onEnter(node) ?? node; @@ -323,14 +329,12 @@ const visitNode = (value: unknown, options: VisitOptions): unknown => { }; const isUnbalancedLogicalTree = (node: AstNode): boolean => { - if (node.type !== 'LogicalExpression' || !isAstNode(node.right)) { + if (node.type !== 'LogicalExpression') { return false; } - return ( - node.right.type === 'LogicalExpression' && - node.operator === node.right.operator - ); + const right = node.right as AstNode; + return right.type === 'LogicalExpression' && node.operator === right.operator; }; const rebalanceLogicalTree = (node: AstNode): AstNode => { @@ -338,10 +342,10 @@ const rebalanceLogicalTree = (node: AstNode): AstNode => { return node; } - const left = asAstNode(node.left); - const right = asAstNode(node.right); - const rightLeft = asAstNode(right.left); - const rightRight = asAstNode(right.right); + const left = node.left as AstNode; + const right = node.right as AstNode; + const rightLeft = right.left as AstNode; + const rightRight = right.right as AstNode; return rebalanceLogicalTree({ type: 'LogicalExpression', @@ -387,7 +391,7 @@ const postprocess = ( switch (node.type) { case 'ParenthesizedExpression': { - const expression = asAstNode(node.expression); + const expression = node.expression as AstNode; const start = locStart(node); // SWC Next comments are in source order, so these end offsets are sorted. @@ -411,15 +415,6 @@ const postprocess = ( return expression; } - case 'TemplateLiteral': { - const expressions = node.expressions as unknown[]; - const quasis = node.quasis as unknown[]; - if (expressions.length !== quasis.length - 1) { - throw new Error('Malformed template literal.'); - } - break; - } - case 'TemplateElement': { if (astType === 'swc-next-ts') { const start = locStart(node) + 1; @@ -430,17 +425,13 @@ const postprocess = ( } case 'TSParenthesizedType': - return asAstNode(node.typeAnnotation); - - case 'TopicReference': - ast.extra = withExtra(ast, { __isUsingHackPipeline: true }); - break; + return node.typeAnnotation as AstNode; case 'TSUnionType': case 'TSIntersectionType': { const types = node.types as unknown[]; if (types.length === 1) { - return asAstNode(types[0]); + return types[0] as AstNode; } break; } diff --git a/packages/rstack/tests/fmt/swcNextPlugin.test.ts b/packages/rstack/tests/fmt/swcNextPlugin.test.ts index ae49ed9..d236bdb 100644 --- a/packages/rstack/tests/fmt/swcNextPlugin.test.ts +++ b/packages/rstack/tests/fmt/swcNextPlugin.test.ts @@ -355,6 +355,15 @@ test('reports SWC Next diagnostics with Prettier locations', async () => { }); }); +test.each(['swc-next', 'swc-next-ts'] as const)( + 'rejects malformed template literals with %s diagnostics', + async (parser) => { + await expect( + formatWithSwcNext('const value = `value: ${}`', { parser }), + ).rejects.toBeInstanceOf(SyntaxError); + }, +); + test.each(['js', 'jsx', 'ts', 'tsx'])( 'uses SWC Next by default for .%s files', async (extension) => { From de0a6bd014ed9c358fd6dba2c69a78ecf3b8016c Mon Sep 17 00:00:00 2001 From: sooniter Date: Mon, 28 Sep 2026 19:22:18 +0800 Subject: [PATCH 2/7] test(fmt): cover postprocess normalization --- .../rstack/tests/fmt/swcNextPlugin.test.ts | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/rstack/tests/fmt/swcNextPlugin.test.ts b/packages/rstack/tests/fmt/swcNextPlugin.test.ts index d236bdb..a5e80c7 100644 --- a/packages/rstack/tests/fmt/swcNextPlugin.test.ts +++ b/packages/rstack/tests/fmt/swcNextPlugin.test.ts @@ -144,6 +144,12 @@ test.each([ source: 'type Value = (((string | number)));', expected: 'type Value = string | number;\n', }, + { + name: 'single-member unions with comments', + parser: 'swc-next-ts' as const, + source: 'type Value = | // value\nstring;', + expected: 'type Value =\n // value\n string;\n', + }, { name: 'TypeScript template expressions', parser: 'swc-next-ts' as const, @@ -167,6 +173,20 @@ test.each([ ).resolves.toBe(fixture.expected); }); +test.each(['swc-next', 'swc-next-ts'] as const)( + 'normalizes nested template expressions with %s', + async (parser) => { + const source = + 'const result=`outer ${(`inner ${(a || (b || c))}`)} ${/** @type {Foo} */ (value)}`'; + const formatted = await formatWithSwcNext(source, { parser }); + + expect(formatted).toBe( + 'const result = `outer ${`inner ${a || b || c}`} ${/** @type {Foo} */ (value)}`;\n', + ); + expect(await formatWithSwcNext(formatted, { parser })).toBe(formatted); + }, +); + test('reuses Prettier options and pragma handling', async () => { await expect( formatWithSwcNext('/** @format */\nconst value={answer:"yes"}', { @@ -355,15 +375,6 @@ test('reports SWC Next diagnostics with Prettier locations', async () => { }); }); -test.each(['swc-next', 'swc-next-ts'] as const)( - 'rejects malformed template literals with %s diagnostics', - async (parser) => { - await expect( - formatWithSwcNext('const value = `value: ${}`', { parser }), - ).rejects.toBeInstanceOf(SyntaxError); - }, -); - test.each(['js', 'jsx', 'ts', 'tsx'])( 'uses SWC Next by default for .%s files', async (extension) => { From 952ac82b93e688db4ce2c93fc7699498b6774f36 Mon Sep 17 00:00:00 2001 From: sooniter Date: Mon, 28 Sep 2026 19:25:23 +0800 Subject: [PATCH 3/7] test(fmt): group postprocess coverage in a suite --- .../rstack/tests/fmt/swcNextPlugin.test.ts | 200 +++++++++--------- 1 file changed, 101 insertions(+), 99 deletions(-) diff --git a/packages/rstack/tests/fmt/swcNextPlugin.test.ts b/packages/rstack/tests/fmt/swcNextPlugin.test.ts index a5e80c7..dccc9ad 100644 --- a/packages/rstack/tests/fmt/swcNextPlugin.test.ts +++ b/packages/rstack/tests/fmt/swcNextPlugin.test.ts @@ -4,7 +4,7 @@ import { type Options, type ParserOptions, } from 'prettier'; -import { expect, test } from 'rstack/test'; +import { describe, expect, test } from 'rstack/test'; import { getPrettierPlugins } from '../../src/fmt/prettierPlugins.ts'; import { swcNextPlugin } from '../../src/fmt/swcNextPlugin.ts'; @@ -107,85 +107,108 @@ test.each(['example.d.ts', 'example.d.mts', 'example.d.cts'])( }, ); -test.each([ - { - name: 'hashbangs and unicode locations', - parser: 'swc-next' as const, - source: '#!/usr/bin/env node\n// 中文 😀\nconst 你好={值:"😀"}', - expected: '#!/usr/bin/env node\n// 中文 😀\nconst 你好 = { 值: "😀" };\n', - }, - { - name: 'Closure-style type casts', - parser: 'swc-next' as const, - source: '/** @type {Foo} */ (value).method()', - expected: '/** @type {Foo} */ (value).method();\n', - }, - { - name: 'comments before semicolons', - parser: 'swc-next' as const, - source: 'foo /* trailing */ ;', - expected: 'foo; /* trailing */\n', - }, - { - name: 'adjacent multiline JSDoc comments', - parser: 'swc-next' as const, - source: '/**\n * outer\n *//**\n * inner\n */\nfoo()', - expected: '/**\n * outer\n *//**\n * inner\n */\nfoo();\n', - }, - { - name: 'right-nested logical expressions', - parser: 'swc-next' as const, - source: 'const value = a || (b || c)', - expected: 'const value = a || b || c;\n', - }, - { - name: 'parenthesized TypeScript types', - parser: 'swc-next-ts' as const, - source: 'type Value = (((string | number)));', - expected: 'type Value = string | number;\n', - }, - { - name: 'single-member unions with comments', - parser: 'swc-next-ts' as const, - source: 'type Value = | // value\nstring;', - expected: 'type Value =\n // value\n string;\n', - }, - { - name: 'TypeScript template expressions', - parser: 'swc-next-ts' as const, - source: 'const result = `value: ${foo satisfies string}`', - expected: 'const result = `value: ${foo satisfies string}`;\n', - }, - { - name: 'TSX expressions', - parser: 'swc-next-ts' as const, - filepath: 'example.tsx', - source: 'const view=({(item)})', - expected: - 'const view = {item};\n', - }, -])('normalizes $name for the ESTree printer', async (fixture) => { - await expect( - formatWithSwcNext(fixture.source, { - filepath: fixture.filepath, - parser: fixture.parser, - }), - ).resolves.toBe(fixture.expected); -}); +describe('postprocess', () => { + test.each([ + { + name: 'hashbangs and unicode locations', + parser: 'swc-next' as const, + source: '#!/usr/bin/env node\n// 中文 😀\nconst 你好={值:"😀"}', + expected: '#!/usr/bin/env node\n// 中文 😀\nconst 你好 = { 值: "😀" };\n', + }, + { + name: 'Closure-style type casts', + parser: 'swc-next' as const, + source: '/** @type {Foo} */ (value).method()', + expected: '/** @type {Foo} */ (value).method();\n', + }, + { + name: 'comments before semicolons', + parser: 'swc-next' as const, + source: 'foo /* trailing */ ;', + expected: 'foo; /* trailing */\n', + }, + { + name: 'adjacent multiline JSDoc comments', + parser: 'swc-next' as const, + source: '/**\n * outer\n *//**\n * inner\n */\nfoo()', + expected: '/**\n * outer\n *//**\n * inner\n */\nfoo();\n', + }, + { + name: 'right-nested logical expressions', + parser: 'swc-next' as const, + source: 'const value = a || (b || c)', + expected: 'const value = a || b || c;\n', + }, + { + name: 'parenthesized TypeScript types', + parser: 'swc-next-ts' as const, + source: 'type Value = (((string | number)));', + expected: 'type Value = string | number;\n', + }, + { + name: 'single-member unions with comments', + parser: 'swc-next-ts' as const, + source: 'type Value = | // value\nstring;', + expected: 'type Value =\n // value\n string;\n', + }, + { + name: 'TypeScript template expressions', + parser: 'swc-next-ts' as const, + source: 'const result = `value: ${foo satisfies string}`', + expected: 'const result = `value: ${foo satisfies string}`;\n', + }, + { + name: 'TSX expressions', + parser: 'swc-next-ts' as const, + filepath: 'example.tsx', + source: 'const view=({(item)})', + expected: + 'const view = {item};\n', + }, + ])('normalizes $name for the ESTree printer', async (fixture) => { + await expect( + formatWithSwcNext(fixture.source, { + filepath: fixture.filepath, + parser: fixture.parser, + }), + ).resolves.toBe(fixture.expected); + }); -test.each(['swc-next', 'swc-next-ts'] as const)( - 'normalizes nested template expressions with %s', - async (parser) => { - const source = - 'const result=`outer ${(`inner ${(a || (b || c))}`)} ${/** @type {Foo} */ (value)}`'; - const formatted = await formatWithSwcNext(source, { parser }); + test.each(['swc-next', 'swc-next-ts'] as const)( + 'normalizes nested template expressions with %s', + async (parser) => { + const source = + 'const result=`outer ${(`inner ${(a || (b || c))}`)} ${/** @type {Foo} */ (value)}`'; + const formatted = await formatWithSwcNext(source, { parser }); + + expect(formatted).toBe( + 'const result = `outer ${`inner ${a || b || c}`} ${/** @type {Foo} */ (value)}`;\n', + ); + expect(await formatWithSwcNext(formatted, { parser })).toBe(formatted); + }, + ); - expect(formatted).toBe( - 'const result = `outer ${`inner ${a || b || c}`} ${/** @type {Foo} */ (value)}`;\n', - ); - expect(await formatWithSwcNext(formatted, { parser })).toBe(formatted); - }, -); + test('matches the official hashbang AST shape', async () => { + const parser = swcNextPlugin.parsers?.['swc-next']; + if (!parser) { + throw new Error('The SWC Next parser is not registered.'); + } + + const options = { filepath: 'example.js' } as ParserOptions; + const astWithoutHashbang = (await parser.parse( + 'const value = 1', + options, + )) as Record; + const astWithHashbang = (await parser.parse( + '#!/usr/bin/env node\nconst value = 1', + options, + )) as Record; + + expect(Object.hasOwn(astWithoutHashbang, 'hashbang')).toBe(true); + expect(astWithoutHashbang.hashbang).toBeNull(); + expect(Object.hasOwn(astWithHashbang, 'hashbang')).toBe(false); + }); +}); test('reuses Prettier options and pragma handling', async () => { await expect( @@ -335,27 +358,6 @@ test('supports CommonJS source semantics for .cjs files', async () => { ).resolves.toBe('return require("example");\n'); }); -test('matches the official hashbang AST shape', async () => { - const parser = swcNextPlugin.parsers?.['swc-next']; - if (!parser) { - throw new Error('The SWC Next parser is not registered.'); - } - - const options = { filepath: 'example.js' } as ParserOptions; - const astWithoutHashbang = (await parser.parse( - 'const value = 1', - options, - )) as Record; - const astWithHashbang = (await parser.parse( - '#!/usr/bin/env node\nconst value = 1', - options, - )) as Record; - - expect(Object.hasOwn(astWithoutHashbang, 'hashbang')).toBe(true); - expect(astWithoutHashbang.hashbang).toBeNull(); - expect(Object.hasOwn(astWithHashbang, 'hashbang')).toBe(false); -}); - test('reports SWC Next diagnostics with Prettier locations', async () => { const error = await formatWithSwcNext('\n\nconst = 1', { parser: 'swc-next-ts', From ab774bf16fa3b3cfcbed612949654645c25f397f Mon Sep 17 00:00:00 2001 From: sooniter Date: Mon, 28 Sep 2026 19:26:45 +0800 Subject: [PATCH 4/7] test(fmt): clarify postprocess suite description --- packages/rstack/tests/fmt/swcNextPlugin.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rstack/tests/fmt/swcNextPlugin.test.ts b/packages/rstack/tests/fmt/swcNextPlugin.test.ts index dccc9ad..70fb6b2 100644 --- a/packages/rstack/tests/fmt/swcNextPlugin.test.ts +++ b/packages/rstack/tests/fmt/swcNextPlugin.test.ts @@ -107,7 +107,7 @@ test.each(['example.d.ts', 'example.d.mts', 'example.d.cts'])( }, ); -describe('postprocess', () => { +describe('should match upstream postprocess behavior', () => { test.each([ { name: 'hashbangs and unicode locations', From e99530101f3e8467d0ef2555648a9a03d7abf009 Mon Sep 17 00:00:00 2001 From: sooniter Date: Mon, 28 Sep 2026 19:28:05 +0800 Subject: [PATCH 5/7] docs(fmt): clarify local plugin adapter differences --- packages/rstack/src/fmt/swcNextPlugin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rstack/src/fmt/swcNextPlugin.ts b/packages/rstack/src/fmt/swcNextPlugin.ts index 4e7d366..f38886e 100644 --- a/packages/rstack/src/fmt/swcNextPlugin.ts +++ b/packages/rstack/src/fmt/swcNextPlugin.ts @@ -1,7 +1,8 @@ // Differences from @prettier/plugin-yuku at the original port's upstream revision: // https://github.com/prettier/prettier/blob/315f28198200d7678dadd3fd5eece499b127ff2a/packages/plugin-yuku/index.js // https://github.com/prettier/prettier/blob/315f28198200d7678dadd3fd5eece499b127ff2a/src/language-js/parse/postprocess/index.js -// - Reuses the ESTree printer and keeps only SWC Next's JS/TS normalization. +// - Bundles a local copy of the plugin adapter, removing branches unrelated to SWC Next. +// - Reuses Prettier's ESTree printer to avoid bundling a duplicate. // - Masks comments in one pass and uses binary search for type-cast comments. // - Omits Hack pipelines (unsupported) and template-shape checks (guaranteed by SWC Next). // - Adds babel/typescript parser aliases, with a Babel File root for Vue. From 53047b47aca42833d8584617b3b8c368c9103f85 Mon Sep 17 00:00:00 2001 From: sooniter Date: Mon, 28 Sep 2026 19:30:39 +0800 Subject: [PATCH 6/7] docs(fmt): clarify upstream adapter differences --- packages/rstack/src/fmt/swcNextPlugin.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/rstack/src/fmt/swcNextPlugin.ts b/packages/rstack/src/fmt/swcNextPlugin.ts index f38886e..b43e129 100644 --- a/packages/rstack/src/fmt/swcNextPlugin.ts +++ b/packages/rstack/src/fmt/swcNextPlugin.ts @@ -1,11 +1,12 @@ -// Differences from @prettier/plugin-yuku at the original port's upstream revision: +// Adapted from @prettier/plugin-yuku: // https://github.com/prettier/prettier/blob/315f28198200d7678dadd3fd5eece499b127ff2a/packages/plugin-yuku/index.js // https://github.com/prettier/prettier/blob/315f28198200d7678dadd3fd5eece499b127ff2a/src/language-js/parse/postprocess/index.js -// - Bundles a local copy of the plugin adapter, removing branches unrelated to SWC Next. -// - Reuses Prettier's ESTree printer to avoid bundling a duplicate. -// - Masks comments in one pass and uses binary search for type-cast comments. -// - Omits Hack pipelines (unsupported) and template-shape checks (guaranteed by SWC Next). -// - Adds babel/typescript parser aliases, with a Babel File root for Vue. +// +// Differences from upstream: +// - Bundles the adapter locally and reuses Prettier's printer to avoid duplication. +// - Uses SWC Next instead of Yuku, removing unused branches and redundant checks. +// - Avoids repeated source copying and linear searches when processing comments. +// - Registers babel/typescript aliases for other plugins; uses Babel's File root for Vue. import * as prettierEstreePlugin from 'prettier/plugins/estree'; import type { Parser, ParserOptions, Plugin } from 'prettier'; From 7e2eee7dab575d5fbc2ec0b1e26b262197081dbf Mon Sep 17 00:00:00 2001 From: sooniter Date: Mon, 28 Sep 2026 19:33:06 +0800 Subject: [PATCH 7/7] test(fmt): name Prettier in postprocess suite --- packages/rstack/tests/fmt/swcNextPlugin.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rstack/tests/fmt/swcNextPlugin.test.ts b/packages/rstack/tests/fmt/swcNextPlugin.test.ts index 70fb6b2..2f4b884 100644 --- a/packages/rstack/tests/fmt/swcNextPlugin.test.ts +++ b/packages/rstack/tests/fmt/swcNextPlugin.test.ts @@ -107,7 +107,7 @@ test.each(['example.d.ts', 'example.d.mts', 'example.d.cts'])( }, ); -describe('should match upstream postprocess behavior', () => { +describe("should match Prettier's postprocess behavior", () => { test.each([ { name: 'hashbangs and unicode locations',