diff --git a/packages/rstack/src/fmt/swcNextPlugin.ts b/packages/rstack/src/fmt/swcNextPlugin.ts
index e8e335d..b43e129 100644
--- a/packages/rstack/src/fmt/swcNextPlugin.ts
+++ b/packages/rstack/src/fmt/swcNextPlugin.ts
@@ -1,5 +1,12 @@
-// Adapted from @prettier/plugin-yuku to use SWC Next:
-// https://github.com/prettier/prettier/tree/main/packages/plugin-yuku
+// 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
+//
+// 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';
@@ -305,7 +312,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 +331,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 +344,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 +393,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 +417,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 +427,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..2f4b884 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,64 +107,107 @@ 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: '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("should match Prettier's postprocess behavior", () => {
+ 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 });
+
+ 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 () => {
@@ -315,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',