From dae1f3145d3c9f142192f802ccde68a5424c2468 Mon Sep 17 00:00:00 2001 From: sooniter Date: Mon, 7 Sep 2026 16:19:52 +0800 Subject: [PATCH 1/9] feat(fmt): prototype SWC Next 0.2.2 npm integration --- packages/rstack/SWC_NEXT_POC.md | 30 ++ packages/rstack/THIRD_PARTY_NOTICES.md | 4 +- packages/rstack/package.json | 4 +- packages/rstack/src/fmt/cacheIdentity.ts | 1 + packages/rstack/src/fmt/prettierPlugins.ts | 6 +- packages/rstack/src/fmt/swcNextParser.ts | 25 ++ .../fmt/{yukuPlugin.ts => swcNextPlugin.ts} | 57 ++-- .../rstack/tests/fmt/cacheIdentity.test.ts | 1 + ...kuPlugin.test.ts => swcNextPlugin.test.ts} | 125 +++++--- pnpm-lock.yaml | 290 +++++++++--------- pnpm-workspace.yaml | 17 +- website/docs/en/guide/formatting.mdx | 4 +- website/docs/zh/guide/formatting.mdx | 4 +- 13 files changed, 344 insertions(+), 224 deletions(-) create mode 100644 packages/rstack/SWC_NEXT_POC.md create mode 100644 packages/rstack/src/fmt/swcNextParser.ts rename packages/rstack/src/fmt/{yukuPlugin.ts => swcNextPlugin.ts} (92%) rename packages/rstack/tests/fmt/{yukuPlugin.test.ts => swcNextPlugin.test.ts} (70%) diff --git a/packages/rstack/SWC_NEXT_POC.md b/packages/rstack/SWC_NEXT_POC.md new file mode 100644 index 00000000..c86bedd7 --- /dev/null +++ b/packages/rstack/SWC_NEXT_POC.md @@ -0,0 +1,30 @@ +# SWC next formatter PoC + +This branch replaces the default Yuku parser in `rs fmt` with SWC Next 0.2.2 while retaining Prettier's ESTree printer and the existing formatting adapter. + +`@swc-next/parser@0.2.2` supplies its own platform-specific native binding and decodes its output internally. The rstack native binding is unchanged. + +Inferred JavaScript, JSX, TypeScript, TSX, and embedded script formatting use SWC Next. Explicit `babel` and `typescript` options still select Prettier's own parsers, and project plugins retain precedence. The experimental explicit parser names are `swc-next` and `swc-next-ts`, replacing `yuku` and `yuku-ts`. + +The cache namespace includes the parser version and integration route so switching between the original formatter and either PoC cannot reuse stale formatting results. The decoder and Rust serializer must be upgraded together; all SWC Next packages in this PoC are locked to 0.2.2. + +## Try it + +```sh +pnpm install +pnpm build +pnpm --filter rstack build:native +printf 'const view=' | pnpm exec rs fmt --stdin-filepath example.tsx +pnpm test +pnpm check +``` + +## Compatibility and limits + +SWC Next 0.2.2 reports diagnostics for function implementations in declaration files, restoring the formatter's existing rejection behavior. It also parses the typed arrow callback inside the conditional expression that previously failed when formatting Rsbuild's `inspectConfig.ts` with 0.2.0. Tests cover both cases, alongside valid declarations, comments, Unicode locations, JSX/TSX, CommonJS, pragmas, and syntax errors. Parser errors stop formatting without falling back to another parser. + +Validation is on macOS arm64 with the repository's Node.js, pnpm, and Rust versions. The complete formatter/CLI suite covers worker execution, stdin, LSP, Vue, Svelte, and caching. Cross-platform binaries and formatter-wide conformance against Prettier's upstream corpus have not been validated. No throughput advantage is claimed by this PoC. + +## Upstream + +[SWC Next 0.2.2 source](https://github.com/swc-project/swc-next/tree/v0.2.2) diff --git a/packages/rstack/THIRD_PARTY_NOTICES.md b/packages/rstack/THIRD_PARTY_NOTICES.md index a4f4fa3e..4948fef4 100644 --- a/packages/rstack/THIRD_PARTY_NOTICES.md +++ b/packages/rstack/THIRD_PARTY_NOTICES.md @@ -29,9 +29,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## Prettier yuku parser adapter +## Prettier SWC next parser adapter -The local Yuku parser adapter includes portions derived from +The local SWC Next parser adapter includes portions derived from [@prettier/plugin-yuku](https://github.com/prettier/prettier/tree/main/packages/plugin-yuku) and Prettier's JavaScript parser postprocessing. The adapter reuses the public ESTree printer, formatter options, and parser utilities from the installed diff --git a/packages/rstack/package.json b/packages/rstack/package.json index eec8420a..57c4996e 100644 --- a/packages/rstack/package.json +++ b/packages/rstack/package.json @@ -77,9 +77,9 @@ "@rslib/core": "catalog:", "@rslint/core": "catalog:", "@rstest/core": "catalog:", + "@swc-next/parser": "catalog:", "prettier": "catalog:", - "tinypool": "catalog:", - "yuku-parser": "catalog:" + "tinypool": "catalog:" }, "devDependencies": { "@napi-rs/cli": "catalog:", diff --git a/packages/rstack/src/fmt/cacheIdentity.ts b/packages/rstack/src/fmt/cacheIdentity.ts index 81d7bdd6..d41a763b 100644 --- a/packages/rstack/src/fmt/cacheIdentity.ts +++ b/packages/rstack/src/fmt/cacheIdentity.ts @@ -21,6 +21,7 @@ const cacheNamespace: string = JSON.stringify([ fmtCacheVersion, RSTACK_VERSION, PRETTIER_VERSION, + 'swc-next@0.2.2:npm', ]); /** Creates project-relative POSIX cache keys without repeating path setup. */ diff --git a/packages/rstack/src/fmt/prettierPlugins.ts b/packages/rstack/src/fmt/prettierPlugins.ts index 5dc633f5..9c3cb2b2 100644 --- a/packages/rstack/src/fmt/prettierPlugins.ts +++ b/packages/rstack/src/fmt/prettierPlugins.ts @@ -1,6 +1,6 @@ import type { Options as PrettierOptions, Plugin } from 'prettier'; import type { ResolvedFmtOptions } from './types.ts'; -import { yukuPlugin } from './yukuPlugin.ts'; +import { swcNextPlugin } from './swcNextPlugin.ts'; type PrettierPlugins = NonNullable; @@ -15,14 +15,14 @@ const fmtOptionsPlugin = { }, } satisfies Plugin; -const defaultFmtPlugins: PrettierPlugins = [yukuPlugin, fmtOptionsPlugin]; +const defaultFmtPlugins: PrettierPlugins = [swcNextPlugin, fmtOptionsPlugin]; /** Prepends bundled plugins so project plugins can override their parsers. */ const getPrettierPlugins = async ( options: ResolvedFmtOptions, filePath: string, ): Promise => { - // An explicit native parser is also the escape hatch for bypassing Yuku. + // An explicit native parser is also the escape hatch for bypassing SWC Next. const defaultPlugins = options.parser === 'babel' || options.parser === 'typescript' ? [fmtOptionsPlugin] diff --git a/packages/rstack/src/fmt/swcNextParser.ts b/packages/rstack/src/fmt/swcNextParser.ts new file mode 100644 index 00000000..6c9633ee --- /dev/null +++ b/packages/rstack/src/fmt/swcNextParser.ts @@ -0,0 +1,25 @@ +import { + langFromPath, + parseSync as parse, + type ParserOptions, + type ParseResult as SwcParseResult, +} from '@swc-next/parser'; + +export { langFromPath }; +export type { Comment, Diagnostic } from '@swc-next/parser'; +export type Lang = 'js' | 'jsx' | 'ts' | 'tsx' | 'dts'; +export type SourceType = 'module' | 'commonjs'; +export type ParseOptions = { + lang: Lang; + sourceType: SourceType; + preserveParens: true; + comments: 'flat'; +}; + +// Keep the upstream untyped AST behind an unknown boundary. +export type ParseResult = Omit & { + program: unknown; +}; + +export const parseSync = (source: string, options: ParseOptions): ParseResult => + parse(source, options as ParserOptions); diff --git a/packages/rstack/src/fmt/yukuPlugin.ts b/packages/rstack/src/fmt/swcNextPlugin.ts similarity index 92% rename from packages/rstack/src/fmt/yukuPlugin.ts rename to packages/rstack/src/fmt/swcNextPlugin.ts index bd6d74f3..932fe086 100644 --- a/packages/rstack/src/fmt/yukuPlugin.ts +++ b/packages/rstack/src/fmt/swcNextPlugin.ts @@ -2,16 +2,16 @@ import * as prettierEstreePlugin from 'prettier/plugins/estree'; import type { Parser, ParserOptions, Plugin } from 'prettier'; import { langFromPath, - parse as parseWithYuku, + parseSync as parseWithSwcNext, type Comment, type Diagnostic, type ParseOptions, type ParseResult, - type SourceLang, + type Lang, type SourceType, -} from 'yuku-parser'; +} from './swcNextParser.ts'; -const AST_FORMAT = 'estree-yuku'; +const AST_FORMAT = 'estree-swc-next'; const JS_TS_FILE_REGEXP = /\.(?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$/i; const JSX_REGEXP = /^[^"'`]*<\/|^[^/]{2}.*\/>/m; const SOURCE_TYPE_COMBINATIONS: SourceType[] = ['module', 'commonjs']; @@ -158,7 +158,7 @@ const isAstNode = (value: unknown): value is AstNode => const asAstNode = (value: unknown): AstNode => { if (!isAstNode(value)) { - throw new TypeError('Expected a Yuku AST node.'); + throw new TypeError('Expected a SWC Next AST node.'); } return value; }; @@ -220,7 +220,7 @@ const stripComments = ( const chunks: string[] = []; let cursor = 0; - // Yuku returns comments in source order, so mask each range while copying the source only once. + // SWC Next returns comments in source order, so mask each range while copying the source only once. for (const comment of comments) { const start = locStart(comment); const end = locEnd(comment); @@ -356,7 +356,7 @@ const postprocess = ( ast: AstNode, comments: PrettierComment[], text: string, - astType: 'yuku-js' | 'yuku-ts', + astType: 'swc-next-js' | 'swc-next-ts', ): AstNode => { mergeNestedJsdocComments(comments); @@ -384,7 +384,7 @@ const postprocess = ( const expression = asAstNode(node.expression); const start = locStart(node); - // Yuku comments are in source order, so these end offsets are sorted. + // SWC Next comments are in source order, so these end offsets are sorted. typeCastCommentEnds ??= comments .filter(isTypeCastComment) .map((comment) => locEnd(comment)); @@ -415,7 +415,7 @@ const postprocess = ( } case 'TemplateElement': { - if (astType === 'yuku-ts') { + if (astType === 'swc-next-ts') { const start = locStart(node) + 1; const end = locEnd(node) - (node.tail ? 1 : 2); node.range = [start, end]; @@ -482,11 +482,13 @@ const createParseError = (error: Diagnostic, text: string): SyntaxError => { ); }; -const parseWithOptions = (text: string, options: ParseOptions): ParseResult => { - const result = parseWithYuku(text, { +const parseWithOptions = ( + text: string, + options: Pick, +): ParseResult => { + const result = parseWithSwcNext(text, { preserveParens: true, - semanticErrors: false, - attachComments: false, + comments: 'flat', ...options, }); @@ -509,10 +511,7 @@ const getSourceType = (filepath: string): SourceType | undefined => { return undefined; }; -const getLanguageCombinations = ( - text: string, - filepath: string, -): SourceLang[] => { +const getLanguageCombinations = (text: string, filepath: string): Lang[] => { const normalizedPath = filepath.toLowerCase(); if (JS_TS_FILE_REGEXP.test(normalizedPath)) { @@ -542,7 +541,7 @@ const tryCombinations = (combinations: (() => ParseResult)[]): ParseResult => { throw firstError; } - throw new Error('No Yuku parser combinations were provided.'); + throw new Error('No SWC Next parser combinations were provided.'); }; const parseJavaScript = ( @@ -558,7 +557,7 @@ const parseJavaScript = ( ); const { program, comments } = tryCombinations(combinations); - return postprocess(program as unknown as AstNode, comments, text, 'yuku-js'); + return postprocess(asAstNode(program), comments, text, 'swc-next-js'); }; const parseTypeScript = ( @@ -576,7 +575,7 @@ const parseTypeScript = ( ); const { program, comments } = tryCombinations(combinations); - return postprocess(program as unknown as AstNode, comments, text, 'yuku-ts'); + return postprocess(asAstNode(program), comments, text, 'swc-next-ts'); }; const createParser = ( @@ -611,23 +610,23 @@ const parseBabel = (text: string, options: ParserOptions): AstNode => { }; }; -const yukuParser = createParser(parseJavaScript); -const yukuBabelParser = createParser(parseBabel); -const yukuTypeScriptParser = createParser(parseTypeScript); +const swcNextParser = createParser(parseJavaScript); +const swcNextBabelParser = createParser(parseBabel); +const swcNextTypeScriptParser = createParser(parseTypeScript); -const yukuPlugin: Plugin = { +const swcNextPlugin: Plugin = { options: estreePlugin.options, parsers: { // Prettier resolves parsers from the last plugin that provides the name. // Project plugins are loaded after this one, so parser wrappers take priority. - babel: yukuBabelParser, - typescript: yukuTypeScriptParser, - yuku: yukuParser, - 'yuku-ts': yukuTypeScriptParser, + babel: swcNextBabelParser, + typescript: swcNextTypeScriptParser, + 'swc-next': swcNextParser, + 'swc-next-ts': swcNextTypeScriptParser, }, printers: { [AST_FORMAT]: estreePrinter, }, }; -export { yukuPlugin }; +export { swcNextPlugin }; diff --git a/packages/rstack/tests/fmt/cacheIdentity.test.ts b/packages/rstack/tests/fmt/cacheIdentity.test.ts index 29f2bebb..9af2d0a4 100644 --- a/packages/rstack/tests/fmt/cacheIdentity.test.ts +++ b/packages/rstack/tests/fmt/cacheIdentity.test.ts @@ -88,6 +88,7 @@ test('includes formatter implementation versions in the namespace', () => { fmtCacheVersion, pkgJson.version, prettierPkgJson.version, + 'swc-next@0.2.2:npm', ]); }); diff --git a/packages/rstack/tests/fmt/yukuPlugin.test.ts b/packages/rstack/tests/fmt/swcNextPlugin.test.ts similarity index 70% rename from packages/rstack/tests/fmt/yukuPlugin.test.ts rename to packages/rstack/tests/fmt/swcNextPlugin.test.ts index 59e7e395..ae49ed9a 100644 --- a/packages/rstack/tests/fmt/yukuPlugin.test.ts +++ b/packages/rstack/tests/fmt/swcNextPlugin.test.ts @@ -6,28 +6,33 @@ import { } from 'prettier'; import { expect, test } from 'rstack/test'; import { getPrettierPlugins } from '../../src/fmt/prettierPlugins.ts'; -import { yukuPlugin } from '../../src/fmt/yukuPlugin.ts'; +import { swcNextPlugin } from '../../src/fmt/swcNextPlugin.ts'; -const formatWithYuku = ( +const formatWithSwcNext = ( source: string, - options: Options & { parser: 'yuku' | 'yuku-ts' }, + options: Options & { parser: 'swc-next' | 'swc-next-ts' }, ): Promise => format(source, { - plugins: [yukuPlugin], + plugins: [swcNextPlugin], ...options, filepath: - options.filepath ?? `example.${options.parser === 'yuku' ? 'js' : 'ts'}`, + options.filepath ?? + `example.${options.parser === 'swc-next' ? 'js' : 'ts'}`, }); test('overrides the default JavaScript and TypeScript parsers', async () => { - expect(yukuPlugin.languages).toBeUndefined(); - expect(yukuPlugin.parsers?.babel).not.toBe(yukuPlugin.parsers?.yuku); - expect(yukuPlugin.parsers?.typescript).toBe(yukuPlugin.parsers?.['yuku-ts']); + expect(swcNextPlugin.languages).toBeUndefined(); + expect(swcNextPlugin.parsers?.babel).not.toBe( + swcNextPlugin.parsers?.['swc-next'], + ); + expect(swcNextPlugin.parsers?.typescript).toBe( + swcNextPlugin.parsers?.['swc-next-ts'], + ); await expect( Promise.all( ['js', 'jsx', 'ts', 'tsx'].map(async (extension) => - getFileInfo(`example.${extension}`, { plugins: [yukuPlugin] }), + getFileInfo(`example.${extension}`, { plugins: [swcNextPlugin] }), ), ), ).resolves.toEqual([ @@ -39,9 +44,9 @@ test('overrides the default JavaScript and TypeScript parsers', async () => { }); test('matches the native Babel AST root shape', async () => { - const parser = yukuPlugin.parsers?.babel; + const parser = swcNextPlugin.parsers?.babel; if (!parser) { - throw new Error('The Babel-compatible Yuku parser is not registered.'); + throw new Error('The Babel-compatible SWC Next parser is not registered.'); } const ast = (await parser.parse('// comment\nconst value = 1', { @@ -63,15 +68,15 @@ test.each(['babel', 'typescript'] as const)( { parser }, `example.${parser === 'babel' ? 'js' : 'ts'}`, ), - ).resolves.not.toContain(yukuPlugin); + ).resolves.not.toContain(swcNextPlugin); }, ); test('parses JSX in JavaScript files', async () => { await expect( - formatWithYuku('const view=', { + formatWithSwcNext('const view=', { filepath: 'example.js', - parser: 'yuku', + parser: 'swc-next', }), ).resolves.toBe('const view = ;\n'); }); @@ -80,9 +85,9 @@ test.each(['example.ts', 'example.mts', 'example.cts'])( 'rejects JSX syntax in %s', async (filepath) => { await expect( - formatWithYuku('const view=', { + formatWithSwcNext('const view=', { filepath, - parser: 'yuku-ts', + parser: 'swc-next-ts', }), ).rejects.toThrow(); }, @@ -92,9 +97,9 @@ test.each(['example.d.ts', 'example.d.mts', 'example.d.cts'])( 'rejects function implementations in %s', async (filepath) => { await expect( - formatWithYuku('export function value() { return 1; }', { + formatWithSwcNext('export function value() { return 1; }', { filepath, - parser: 'yuku-ts', + parser: 'swc-next-ts', }), ).rejects.toThrow( 'An implementation cannot be declared in ambient contexts', @@ -105,49 +110,49 @@ test.each(['example.d.ts', 'example.d.mts', 'example.d.cts'])( test.each([ { name: 'hashbangs and unicode locations', - parser: 'yuku' as const, + 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: 'yuku' as const, + parser: 'swc-next' as const, source: '/** @type {Foo} */ (value).method()', expected: '/** @type {Foo} */ (value).method();\n', }, { name: 'comments before semicolons', - parser: 'yuku' as const, + parser: 'swc-next' as const, source: 'foo /* trailing */ ;', expected: 'foo; /* trailing */\n', }, { name: 'adjacent multiline JSDoc comments', - parser: 'yuku' as const, + 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: 'yuku' as const, + parser: 'swc-next' as const, source: 'const value = a || (b || c)', expected: 'const value = a || b || c;\n', }, { name: 'parenthesized TypeScript types', - parser: 'yuku-ts' as const, + parser: 'swc-next-ts' as const, source: 'type Value = (((string | number)));', expected: 'type Value = string | number;\n', }, { name: 'TypeScript template expressions', - parser: 'yuku-ts' as const, + 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: 'yuku-ts' as const, + parser: 'swc-next-ts' as const, filepath: 'example.tsx', source: 'const view=({(item)})', expected: @@ -155,7 +160,7 @@ test.each([ }, ])('normalizes $name for the ESTree printer', async (fixture) => { await expect( - formatWithYuku(fixture.source, { + formatWithSwcNext(fixture.source, { filepath: fixture.filepath, parser: fixture.parser, }), @@ -164,17 +169,17 @@ test.each([ test('reuses Prettier options and pragma handling', async () => { await expect( - formatWithYuku('/** @format */\nconst value={answer:"yes"}', { - parser: 'yuku', + formatWithSwcNext('/** @format */\nconst value={answer:"yes"}', { + parser: 'swc-next', requirePragma: true, singleQuote: true, }), ).resolves.toBe("/** @format */\nconst value = { answer: 'yes' };\n"); await expect( - formatWithYuku('/** @noformat */\nconst value={answer:"yes"}', { + formatWithSwcNext('/** @noformat */\nconst value={answer:"yes"}', { checkIgnorePragma: true, - parser: 'yuku', + parser: 'swc-next', }), ).resolves.toBe('/** @noformat */\nconst value={answer:"yes"}'); }); @@ -218,9 +223,9 @@ test.each([ ])( 'matches Prettier pragma detection for $source', ({ source, hasPragma, hasIgnorePragma }) => { - const parser = yukuPlugin.parsers?.yuku; + const parser = swcNextPlugin.parsers?.['swc-next']; if (!parser?.hasPragma || !parser.hasIgnorePragma) { - throw new Error('The Yuku parser does not expose pragma handlers.'); + throw new Error('The SWC Next parser does not expose pragma handlers.'); } expect(parser.hasPragma(source)).toBe(hasPragma); @@ -229,9 +234,9 @@ test.each([ ); test('matches Prettier JavaScript location overrides', () => { - const parser = yukuPlugin.parsers?.yuku; + const parser = swcNextPlugin.parsers?.['swc-next']; if (!parser) { - throw new Error('The Yuku parser is not registered.'); + throw new Error('The SWC Next parser is not registered.'); } expect( @@ -303,17 +308,17 @@ test('matches Prettier JavaScript location overrides', () => { test('supports CommonJS source semantics for .cjs files', async () => { await expect( - formatWithYuku('return require("example")', { + formatWithSwcNext('return require("example")', { filepath: 'example.cjs', - parser: 'yuku', + parser: 'swc-next', }), ).resolves.toBe('return require("example");\n'); }); test('matches the official hashbang AST shape', async () => { - const parser = yukuPlugin.parsers?.yuku; + const parser = swcNextPlugin.parsers?.['swc-next']; if (!parser) { - throw new Error('The Yuku parser is not registered.'); + throw new Error('The SWC Next parser is not registered.'); } const options = { filepath: 'example.js' } as ParserOptions; @@ -331,9 +336,9 @@ test('matches the official hashbang AST shape', async () => { expect(Object.hasOwn(astWithHashbang, 'hashbang')).toBe(false); }); -test('reports Yuku diagnostics with Prettier locations', async () => { - const error = await formatWithYuku('\n\nconst = 1', { - parser: 'yuku-ts', +test('reports SWC Next diagnostics with Prettier locations', async () => { + const error = await formatWithSwcNext('\n\nconst = 1', { + parser: 'swc-next-ts', }).catch((error: unknown) => error); expect(error).toBeInstanceOf(SyntaxError); @@ -349,3 +354,39 @@ test('reports Yuku diagnostics with Prettier locations', async () => { end: { column: 8, line: 3 }, }); }); + +test.each(['js', 'jsx', 'ts', 'tsx'])( + 'uses SWC Next by default for .%s files', + async (extension) => { + const plugins = await getPrettierPlugins({}, `example.${extension}`); + expect(plugins).toContain(swcNextPlugin); + await expect( + format('const value = {answer:42}', { + filepath: `example.${extension}`, + plugins, + }), + ).resolves.toBe('const value = { answer: 42 };\n'); + }, +); + +test.each(['example.d.ts', 'example.d.mts', 'example.d.cts'])( + 'formats declarations in %s', + async (filepath) => { + await expect( + formatWithSwcNext('export declare function value(x:string):number', { + filepath, + parser: 'swc-next-ts', + }), + ).resolves.toBe('export declare function value(x: string): number;\n'); + }, +); + +test('formats typed arrow callbacks in conditional expressions', async () => { + const input = + 'const configs = options ? Object.entries(options).map(([name, content]): ConfigItem => ({ name, content })) : undefined;'; + const options = { filepath: 'example.ts', parser: 'swc-next-ts' as const }; + const formatted = await formatWithSwcNext(input, options); + + expect(formatted).toBe(await format(input, { parser: 'typescript' })); + expect(await formatWithSwcNext(formatted, options)).toBe(formatted); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79ab9c85..25b59840 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -216,6 +216,9 @@ catalogs: '@shikijs/transformers': specifier: ^4.4.3 version: 4.4.3 + '@swc-next/parser': + specifier: 0.2.2 + version: 0.2.2 '@testing-library/dom': specifier: ^10.4.2 version: 10.4.2 @@ -306,9 +309,6 @@ catalogs: vscode-languageserver-textdocument: specifier: 1.0.14 version: 1.0.14 - yuku-parser: - specifier: 0.10.2 - version: 0.10.2 importers: @@ -531,15 +531,15 @@ importers: '@rstest/core': specifier: 'catalog:' version: 0.12.0(happy-dom@20.14.5) + '@swc-next/parser': + specifier: 'catalog:' + version: 0.2.2 prettier: specifier: 'catalog:' version: 3.9.7 tinypool: specifier: 'catalog:' version: 2.2.0 - yuku-parser: - specifier: 'catalog:' - version: 0.10.2 devDependencies: '@napi-rs/cli': specifier: 'catalog:' @@ -1852,6 +1852,90 @@ packages: peerDependencies: acorn: ^8.9.0 + '@swc-next/decoder@0.2.2': + resolution: {integrity: sha512-eEGT7Y7StyAQ3M5imF5kkzOJYoiGQZb+CAQ5nLjLVT5npxyaWTiA/3wMYfNpNjQ66eSeCfEQd1mRW8XBhzeEjQ==} + + '@swc-next/parser-binding-darwin-arm64@0.2.2': + resolution: {integrity: sha512-ymR+vu+jmJ0kP6XvfexP3WXFRLaXHEj8bR+coUv5Egk4ujZKjhO4tpKi3wl1S+xrv29DJYlaR+M2EKWeiaA/iQ==} + cpu: [arm64] + os: [darwin] + + '@swc-next/parser-binding-darwin-x64@0.2.2': + resolution: {integrity: sha512-zkVm7R7Jj4xhLIcJAENpSymQviU7ONuBs2XUE3BeSFUDqaCn0d4rQUy7MiopLfPSvMxXcA588cyL+9gpWCb1XA==} + cpu: [x64] + os: [darwin] + + '@swc-next/parser-binding-linux-arm64-gnu@0.2.2': + resolution: {integrity: sha512-mC/u0spVB77KR68Wqj16DF0HTKXac8iNf4TltQhFE1snCy2RZarkxzE1TdL0VP8f8DyuEKe1L4V5F6zFizWDbw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc-next/parser-binding-linux-arm64-musl@0.2.2': + resolution: {integrity: sha512-dVUEHAIN1O8iPudJcIKlGmtjKLSueK6zpICOO0q05dLZQExOku2SYNvy+RvciVSVpnHgqIM6TvuXWXLqvjvv4Q==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc-next/parser-binding-linux-ppc64-gnu@0.2.2': + resolution: {integrity: sha512-lzAiv7DsQhPLUCHlLsFsHgcEvTI0LcJoop5gw8i/q1Y+IuBGGJIuwaEQmgSosR40Kr5YUzlDxntlQzq0LT1kBg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc-next/parser-binding-linux-riscv64-gnu@0.2.2': + resolution: {integrity: sha512-2sXJz0/DWUAz4ZPJYZh6c2mNyIJUzWLqagemEqoxHkp48EE8w7UwIoTyaqP72rd/hTSBEC8yO7/LngS4UV8vgA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@swc-next/parser-binding-linux-riscv64-musl@0.2.2': + resolution: {integrity: sha512-L9BQKEWiJnrJh2JZE9N4FOcOaIP7tnxZtQjNILopQO8q8DkiXjT8Pw/NtCCghffkOqSIkjHZDzfZ1kNqcUO2MQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@swc-next/parser-binding-linux-s390x-gnu@0.2.2': + resolution: {integrity: sha512-FTsesVJke0yOAS46wNjcV+f4giBnJr+z84gH1lFvTFenf7VV85ujSSPAINX4ydp9SICAlxRUMYcVP4aXqeXn7w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc-next/parser-binding-linux-x64-gnu@0.2.2': + resolution: {integrity: sha512-6CbsMxW+FZWWcItT78bHFQJACstmBRU6wAup4sVlJjxvHi5mM98t2ozhkghob+rwOJLuyW80MClOlyUlpzZJmg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc-next/parser-binding-linux-x64-musl@0.2.2': + resolution: {integrity: sha512-aNsEqIEPkocxKBMycMUvC687EEIyoEhytzjwbcP4apXCC5ODOHefLD3XnQuqj+Wo+yKtKH/9bbabAGkBAhMWiw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc-next/parser-binding-win32-arm64-msvc@0.2.2': + resolution: {integrity: sha512-rqTCyFCS1MuZJhfeip+01u06kxtO87NQX/j12u8WDZpibIJQSPdJ8N6njjSswSDmMdYSARtBkbKSmnH2a+rSRA==} + cpu: [arm64] + os: [win32] + + '@swc-next/parser-binding-win32-ia32-msvc@0.2.2': + resolution: {integrity: sha512-dR5ghSUFqFqBnpcOWzMFU2g3e2wP9ggSMuhhEu3/GpEM75OmDIsLCzolHygKBhqbqt12NI1XhLwfQvl0/Q2cLA==} + cpu: [ia32] + os: [win32] + + '@swc-next/parser-binding-win32-x64-msvc@0.2.2': + resolution: {integrity: sha512-R79qkJTjlFB0GFTQNZMl7qE9ZXGgF2PyHEKGTQwZJwAOWMSVoN7t6arqI0Wj6T9YSoW+MzW7RH0RitasF6IC/Q==} + cpu: [x64] + os: [win32] + + '@swc-next/parser@0.2.2': + resolution: {integrity: sha512-KpaqY7JD/dUCYvUJz5R7NIIvVIl9qoAQRxFOBhOVukHeVn2DULEvu8o9+IYyFLVZ6CJDnTIONtT1C/ETxC6Pow==} + peerDependencies: + '@swc-next/parser-binding-wasm': 0.2.2 + peerDependenciesMeta: + '@swc-next/parser-binding-wasm': + optional: true + '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} @@ -2077,75 +2161,6 @@ packages: peerDependencies: react: '>=18.3.1' - '@yuku-parser/binding-android-arm64@0.10.2': - resolution: {integrity: sha512-2VPBU9fRGRAQ2xPAvghnec3oou5Nrxm2Bezhf+13UMspAxQSkF/32r+ySKXSwIPA5/RivumDJDxAxc301Sgicw==} - cpu: [arm64] - os: [android] - - '@yuku-parser/binding-darwin-arm64@0.10.2': - resolution: {integrity: sha512-LD+PMZE51tCYTOss5HBkm3/AE39MvcMDBfWJx7A4yDcjfNbAQDnHZKtzSOuqrswx+TQHY0ws5xn6+fWOwtmfBA==} - cpu: [arm64] - os: [darwin] - - '@yuku-parser/binding-darwin-x64@0.10.2': - resolution: {integrity: sha512-mmZ8cND+AoIIMRERyMinlg5ApHxP23B9jH2B5wT7T+dliPa9rubLxneB/SUjFwyUjGaFnB5G7t4YvpfbO5zbkQ==} - cpu: [x64] - os: [darwin] - - '@yuku-parser/binding-freebsd-x64@0.10.2': - resolution: {integrity: sha512-gVIjaaIddbRfAhHlC8N809wQWml7mxfSVnzaLtzGXObTeFTPAg/YVnQXt1UOhPM1ah5eG/8Y0RUlNpB4GVe7eQ==} - cpu: [x64] - os: [freebsd] - - '@yuku-parser/binding-linux-arm-gnu@0.10.2': - resolution: {integrity: sha512-q/XPPQQAPdlw05aPj30ygBhekmQryGOwxVraBgApjKK8yY1kNQgqq6XCYLF1WHSee+lhxclrTO7W0/bt7dR1GA==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@yuku-parser/binding-linux-arm-musl@0.10.2': - resolution: {integrity: sha512-A+Cb0I1hFF4wilTQpWs79k1aNnj4B2tkrHx3zsuUNF9BtVY2zPZ4yeQEM/zjXrS/qJlNMZVK9NrWJjwdpnTrfg==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@yuku-parser/binding-linux-arm64-gnu@0.10.2': - resolution: {integrity: sha512-aGNSzqIqqphFwAdIFwVvKIyXD1Iy3CxrEJVIZAT87Ecyi4vCmmDD2v2l9h+Gd3/wSy3JZlOI792LJbKAjyy9rQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@yuku-parser/binding-linux-arm64-musl@0.10.2': - resolution: {integrity: sha512-Ddl1sF0rtuCXyHGSOV6dcmdx+ETv1iD+IVFqQuOjzkJZWclxJxdBWX6ZJMRtTiGqGUTbqLEKiTXxpR/Bvqk+2A==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@yuku-parser/binding-linux-x64-gnu@0.10.2': - resolution: {integrity: sha512-/nlcpR6IF5U+0m5L9wvAIXeQa2DT+BXuvqKDsTcOTlcc0B4dHNyqE5hTXsS4hAyimpy2ZK8A1zMNF5hTrjg2cg==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@yuku-parser/binding-linux-x64-musl@0.10.2': - resolution: {integrity: sha512-GX80dxTQD/M/OryyuxJcFzFENzry3cDFPvCFTyWogNWQH3fSHfMrNfpwpl1YzMos1eV9NygK5GSDG7VArQlb4w==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@yuku-parser/binding-win32-arm64@0.10.2': - resolution: {integrity: sha512-agePQBV4VHewiGU0ACSjscZ/hJd283f9JfAF19Gf1rI5+wy5tTgx4GS36i0b3xim15AQ4RCpDRosEvhLZ4zAOw==} - cpu: [arm64] - os: [win32] - - '@yuku-parser/binding-win32-x64@0.10.2': - resolution: {integrity: sha512-s8//CMpgL5+y1lvDCdyh1rwGI5+ytDJywFJRe1vnhI7n0j+caxshNucurikYLLVeQ2SYFex6aPrzgQxkabBQRA==} - cpu: [x64] - os: [win32] - - '@yuku-toolchain/types@0.10.2': - resolution: {integrity: sha512-sSeo4SSSToiS+sSD+bwn/s94EEcaLJ7tG5LCp8gFYC1G5VzxzX7fqB6m9RU1yMD8KTpu6zdU/I1NlQf8hVBJ9Q==} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3410,12 +3425,6 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yuku-ast@0.10.2: - resolution: {integrity: sha512-UnG9mA6giglCvSErft2/40TVFX750Sj1xgwddPLpG7J7rlr/P1wPADg9G2RK+d/1tNLtRVoLdMMOMVBB9561TQ==} - - yuku-parser@0.10.2: - resolution: {integrity: sha512-CgaU0/PPjCAIEZ3WQroosOxTY3eeKldAN3h+vk8pMNz4+jl1CZzBR4pW+K/rRPF112VooCL5FdjJoiMNjDrL2A==} - zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -4514,6 +4523,65 @@ snapshots: dependencies: acorn: 8.17.0 + '@swc-next/decoder@0.2.2': {} + + '@swc-next/parser-binding-darwin-arm64@0.2.2': + optional: true + + '@swc-next/parser-binding-darwin-x64@0.2.2': + optional: true + + '@swc-next/parser-binding-linux-arm64-gnu@0.2.2': + optional: true + + '@swc-next/parser-binding-linux-arm64-musl@0.2.2': + optional: true + + '@swc-next/parser-binding-linux-ppc64-gnu@0.2.2': + optional: true + + '@swc-next/parser-binding-linux-riscv64-gnu@0.2.2': + optional: true + + '@swc-next/parser-binding-linux-riscv64-musl@0.2.2': + optional: true + + '@swc-next/parser-binding-linux-s390x-gnu@0.2.2': + optional: true + + '@swc-next/parser-binding-linux-x64-gnu@0.2.2': + optional: true + + '@swc-next/parser-binding-linux-x64-musl@0.2.2': + optional: true + + '@swc-next/parser-binding-win32-arm64-msvc@0.2.2': + optional: true + + '@swc-next/parser-binding-win32-ia32-msvc@0.2.2': + optional: true + + '@swc-next/parser-binding-win32-x64-msvc@0.2.2': + optional: true + + '@swc-next/parser@0.2.2': + dependencies: + '@swc-next/decoder': 0.2.2 + optionalDependencies: + '@swc-next/parser-binding-darwin-arm64': 0.2.2 + '@swc-next/parser-binding-darwin-x64': 0.2.2 + '@swc-next/parser-binding-linux-arm64-gnu': 0.2.2 + '@swc-next/parser-binding-linux-arm64-musl': 0.2.2 + '@swc-next/parser-binding-linux-ppc64-gnu': 0.2.2 + '@swc-next/parser-binding-linux-riscv64-gnu': 0.2.2 + '@swc-next/parser-binding-linux-riscv64-musl': 0.2.2 + '@swc-next/parser-binding-linux-s390x-gnu': 0.2.2 + '@swc-next/parser-binding-linux-x64-gnu': 0.2.2 + '@swc-next/parser-binding-linux-x64-musl': 0.2.2 + '@swc-next/parser-binding-win32-arm64-msvc': 0.2.2 + '@swc-next/parser-binding-win32-ia32-msvc': 0.2.2 + '@swc-next/parser-binding-win32-x64-msvc': 0.2.2 + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -4684,44 +4752,6 @@ snapshots: react: 19.3.0 unhead: 2.1.17 - '@yuku-parser/binding-android-arm64@0.10.2': - optional: true - - '@yuku-parser/binding-darwin-arm64@0.10.2': - optional: true - - '@yuku-parser/binding-darwin-x64@0.10.2': - optional: true - - '@yuku-parser/binding-freebsd-x64@0.10.2': - optional: true - - '@yuku-parser/binding-linux-arm-gnu@0.10.2': - optional: true - - '@yuku-parser/binding-linux-arm-musl@0.10.2': - optional: true - - '@yuku-parser/binding-linux-arm64-gnu@0.10.2': - optional: true - - '@yuku-parser/binding-linux-arm64-musl@0.10.2': - optional: true - - '@yuku-parser/binding-linux-x64-gnu@0.10.2': - optional: true - - '@yuku-parser/binding-linux-x64-musl@0.10.2': - optional: true - - '@yuku-parser/binding-win32-arm64@0.10.2': - optional: true - - '@yuku-parser/binding-win32-x64@0.10.2': - optional: true - - '@yuku-toolchain/types@0.10.2': {} - acorn-jsx@5.3.2(acorn@8.17.0): dependencies: acorn: 8.17.0 @@ -6255,28 +6285,6 @@ snapshots: yaml@2.9.0: optional: true - yuku-ast@0.10.2: - dependencies: - '@yuku-toolchain/types': 0.10.2 - - yuku-parser@0.10.2: - dependencies: - '@yuku-toolchain/types': 0.10.2 - yuku-ast: 0.10.2 - optionalDependencies: - '@yuku-parser/binding-android-arm64': 0.10.2 - '@yuku-parser/binding-darwin-arm64': 0.10.2 - '@yuku-parser/binding-darwin-x64': 0.10.2 - '@yuku-parser/binding-freebsd-x64': 0.10.2 - '@yuku-parser/binding-linux-arm-gnu': 0.10.2 - '@yuku-parser/binding-linux-arm-musl': 0.10.2 - '@yuku-parser/binding-linux-arm64-gnu': 0.10.2 - '@yuku-parser/binding-linux-arm64-musl': 0.10.2 - '@yuku-parser/binding-linux-x64-gnu': 0.10.2 - '@yuku-parser/binding-linux-x64-musl': 0.10.2 - '@yuku-parser/binding-win32-arm64': 0.10.2 - '@yuku-parser/binding-win32-x64': 0.10.2 - zimmerframe@1.1.4: {} zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 76d7cbbe..9dac5e35 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -59,7 +59,7 @@ catalog: 'typescript': '^7.0.2' 'vscode-languageserver': '10.1.1' 'vscode-languageserver-textdocument': '1.0.14' - yuku-parser: '0.10.2' + '@swc-next/parser': '0.2.2' dedupePeers: true @@ -72,6 +72,21 @@ hoistPattern: [] # Reduce the risk of installing compromised packages minimumReleaseAge: 1440 minimumReleaseAgeExclude: + - '@swc-next/parser@0.2.2' + - '@swc-next/decoder@0.2.2' + - '@swc-next/parser-binding-darwin-x64@0.2.2' + - '@swc-next/parser-binding-darwin-arm64@0.2.2' + - '@swc-next/parser-binding-linux-x64-gnu@0.2.2' + - '@swc-next/parser-binding-linux-x64-musl@0.2.2' + - '@swc-next/parser-binding-win32-x64-msvc@0.2.2' + - '@swc-next/parser-binding-linux-arm64-gnu@0.2.2' + - '@swc-next/parser-binding-linux-ppc64-gnu@0.2.2' + - '@swc-next/parser-binding-linux-s390x-gnu@0.2.2' + - '@swc-next/parser-binding-win32-ia32-msvc@0.2.2' + - '@swc-next/parser-binding-linux-arm64-musl@0.2.2' + - '@swc-next/parser-binding-win32-arm64-msvc@0.2.2' + - '@swc-next/parser-binding-linux-riscv64-gnu@0.2.2' + - '@swc-next/parser-binding-linux-riscv64-musl@0.2.2' - '@yuku-parser/binding-android-arm64@0.8.3' - '@yuku-parser/binding-darwin-arm64@0.8.3' - '@yuku-parser/binding-darwin-x64@0.8.3' diff --git a/website/docs/en/guide/formatting.mdx b/website/docs/en/guide/formatting.mdx index 1bb65218..24864303 100644 --- a/website/docs/en/guide/formatting.mdx +++ b/website/docs/en/guide/formatting.mdx @@ -6,10 +6,10 @@ description: 'Format files with Rstack CLI using Prettier-compatible options, pl import { PackageManagerTabs } from '@rspress/core/theme'; -Rstack CLI includes a formatter built on [Prettier](https://prettier.io/). Compared with running Prettier directly, `rs fmt` offers better performance in two ways: +Rstack CLI includes a formatter built on [Prettier](https://prettier.io/). Compared with running Prettier directly, `rs fmt` includes the following: - **Parallel formatting**: Files are formatted concurrently in a worker pool. -- **Yuku parser**: The high-performance [Yuku](https://yuku.fyi/) parser is used by default for JavaScript, JSX, and TypeScript files. +- **SWC Next parser**: The [SWC Next](https://github.com/swc-project/swc-next) parser is used by default for JavaScript, JSX, and TypeScript files. - **Persistent cache**: Content-based results let later runs skip formatting unchanged files. `rs fmt` supports Prettier options and plugins and adds built-in capabilities such as [sorting package.json fields](#sort-package-json). diff --git a/website/docs/zh/guide/formatting.mdx b/website/docs/zh/guide/formatting.mdx index da96317c..9a743435 100644 --- a/website/docs/zh/guide/formatting.mdx +++ b/website/docs/zh/guide/formatting.mdx @@ -6,10 +6,10 @@ description: '使用 Rstack CLI 格式化文件,支持与 Prettier 兼容的 import { PackageManagerTabs } from '@rspress/core/theme'; -Rstack CLI 提供了基于 [Prettier](https://prettier.io/) 的格式化工具。相比直接使用 Prettier,`rs fmt` 的性能更好,主要得益于以下两点: +Rstack CLI 提供了基于 [Prettier](https://prettier.io/) 的格式化工具。相比直接使用 Prettier,`rs fmt` 提供了以下能力: - **并行格式化**:通过 worker 池并行格式化文件。 -- **Yuku 解析器**:默认使用高性能的 [Yuku](https://yuku.fyi/) 解析器处理 JavaScript、JSX 和 TypeScript 文件。 +- **SWC Next 解析器**:默认使用 [SWC Next](https://github.com/swc-project/swc-next) 解析器处理 JavaScript、JSX 和 TypeScript 文件。 - **持久化缓存**:基于文件内容缓存结果,后续运行可以跳过未变化文件的格式化。 `rs fmt` 兼容 Prettier 的选项和插件,并提供更多内置能力,例如支持[排序 package.json 字段](#sort-package-json)。 From 8b237034b42bb5b07e622b9795bc3292760c7c3e Mon Sep 17 00:00:00 2001 From: sooniter Date: Tue, 22 Sep 2026 12:02:28 +0800 Subject: [PATCH 2/9] fix(fmt): inject SWC Next version into cache namespace --- packages/rstack/rslib.config.ts | 8 ++++++++ packages/rstack/src/fmt/cacheIdentity.ts | 3 ++- packages/rstack/tests/fmt/cacheIdentity.test.ts | 8 +++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/rstack/rslib.config.ts b/packages/rstack/rslib.config.ts index 2f26a5fc..2a4ed9e5 100644 --- a/packages/rstack/rslib.config.ts +++ b/packages/rstack/rslib.config.ts @@ -1,7 +1,14 @@ +import { readFileSync } from 'node:fs'; +import { findPackageJSON } from 'node:module'; import { defineConfig } from '@rslib/core'; import prettierPkgJson from 'prettier/package.json' with { type: 'json' }; import pkgJson from './package.json' with { type: 'json' }; +// @swc-next/parser does not export its package.json. +const swcNextPkgJson = JSON.parse( + readFileSync(findPackageJSON('@swc-next/parser', import.meta.url)!, 'utf8'), +) as { version: string }; + const fullyMinifiedChunks = /(?:fmt(?:Lsp|Plugins)?|sortPackageJsonPlugin|staged)\.js$/; @@ -27,6 +34,7 @@ export default defineConfig({ define: { PRETTIER_VERSION: JSON.stringify(prettierPkgJson.version), RSTACK_VERSION: JSON.stringify(pkgJson.version), + SWC_NEXT_VERSION: JSON.stringify(swcNextPkgJson.version), }, }, output: { diff --git a/packages/rstack/src/fmt/cacheIdentity.ts b/packages/rstack/src/fmt/cacheIdentity.ts index d41a763b..4400c306 100644 --- a/packages/rstack/src/fmt/cacheIdentity.ts +++ b/packages/rstack/src/fmt/cacheIdentity.ts @@ -7,6 +7,7 @@ import type { ResolvedFmtOptions } from './types.ts'; declare const PRETTIER_VERSION: string; declare const RSTACK_VERSION: string; +declare const SWC_NEXT_VERSION: string; type CacheKeyResolver = (filePath: string) => string | undefined; type OptionsHasher = (options: ResolvedFmtOptions) => string | undefined; @@ -21,7 +22,7 @@ const cacheNamespace: string = JSON.stringify([ fmtCacheVersion, RSTACK_VERSION, PRETTIER_VERSION, - 'swc-next@0.2.2:npm', + `swc-next@${SWC_NEXT_VERSION}:npm`, ]); /** Creates project-relative POSIX cache keys without repeating path setup. */ diff --git a/packages/rstack/tests/fmt/cacheIdentity.test.ts b/packages/rstack/tests/fmt/cacheIdentity.test.ts index 9af2d0a4..29988e13 100644 --- a/packages/rstack/tests/fmt/cacheIdentity.test.ts +++ b/packages/rstack/tests/fmt/cacheIdentity.test.ts @@ -1,3 +1,5 @@ +import { readFileSync } from 'node:fs'; +import { findPackageJSON } from 'node:module'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import prettierPkgJson from 'prettier/package.json' with { type: 'json' }; @@ -84,11 +86,15 @@ test('bypasses user plugins and unserializable options', () => { }); test('includes formatter implementation versions in the namespace', () => { + const swcNextPkgJson = JSON.parse( + readFileSync(findPackageJSON('@swc-next/parser', import.meta.url)!, 'utf8'), + ) as { version: string }; + expect(JSON.parse(cacheNamespace)).toEqual([ fmtCacheVersion, pkgJson.version, prettierPkgJson.version, - 'swc-next@0.2.2:npm', + `swc-next@${swcNextPkgJson.version}:npm`, ]); }); From 321867872ef47780e20f8b4bef813013cf8a58fd Mon Sep 17 00:00:00 2001 From: sooniter Date: Tue, 22 Sep 2026 12:04:18 +0800 Subject: [PATCH 3/9] refactor(fmt): simplify SWC Next cache version --- packages/rstack/SWC_NEXT_POC.md | 2 +- packages/rstack/src/fmt/cacheIdentity.ts | 2 +- packages/rstack/tests/fmt/cacheIdentity.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rstack/SWC_NEXT_POC.md b/packages/rstack/SWC_NEXT_POC.md index c86bedd7..af498d7c 100644 --- a/packages/rstack/SWC_NEXT_POC.md +++ b/packages/rstack/SWC_NEXT_POC.md @@ -6,7 +6,7 @@ This branch replaces the default Yuku parser in `rs fmt` with SWC Next 0.2.2 whi Inferred JavaScript, JSX, TypeScript, TSX, and embedded script formatting use SWC Next. Explicit `babel` and `typescript` options still select Prettier's own parsers, and project plugins retain precedence. The experimental explicit parser names are `swc-next` and `swc-next-ts`, replacing `yuku` and `yuku-ts`. -The cache namespace includes the parser version and integration route so switching between the original formatter and either PoC cannot reuse stale formatting results. The decoder and Rust serializer must be upgraded together; all SWC Next packages in this PoC are locked to 0.2.2. +The cache namespace includes the installed parser version so parser upgrades invalidate existing formatting results. The decoder and Rust serializer must be upgraded together; all SWC Next packages in this PoC are locked to 0.2.2. ## Try it diff --git a/packages/rstack/src/fmt/cacheIdentity.ts b/packages/rstack/src/fmt/cacheIdentity.ts index 4400c306..e03d122e 100644 --- a/packages/rstack/src/fmt/cacheIdentity.ts +++ b/packages/rstack/src/fmt/cacheIdentity.ts @@ -22,7 +22,7 @@ const cacheNamespace: string = JSON.stringify([ fmtCacheVersion, RSTACK_VERSION, PRETTIER_VERSION, - `swc-next@${SWC_NEXT_VERSION}:npm`, + SWC_NEXT_VERSION, ]); /** Creates project-relative POSIX cache keys without repeating path setup. */ diff --git a/packages/rstack/tests/fmt/cacheIdentity.test.ts b/packages/rstack/tests/fmt/cacheIdentity.test.ts index 29988e13..cab32674 100644 --- a/packages/rstack/tests/fmt/cacheIdentity.test.ts +++ b/packages/rstack/tests/fmt/cacheIdentity.test.ts @@ -94,7 +94,7 @@ test('includes formatter implementation versions in the namespace', () => { fmtCacheVersion, pkgJson.version, prettierPkgJson.version, - `swc-next@${swcNextPkgJson.version}:npm`, + swcNextPkgJson.version, ]); }); From 90e2857fc05c5e9bf650cc4e7b1569d4bb737f24 Mon Sep 17 00:00:00 2001 From: sooniter Date: Tue, 22 Sep 2026 12:05:59 +0800 Subject: [PATCH 4/9] refactor(fmt): simplify SWC Next PoC setup and attribution --- packages/rstack/SWC_NEXT_POC.md | 30 ------------------- packages/rstack/rslib.config.ts | 9 ++---- packages/rstack/src/fmt/swcNextPlugin.ts | 3 ++ .../rstack/tests/fmt/cacheIdentity.test.ts | 8 ++--- 4 files changed, 7 insertions(+), 43 deletions(-) delete mode 100644 packages/rstack/SWC_NEXT_POC.md diff --git a/packages/rstack/SWC_NEXT_POC.md b/packages/rstack/SWC_NEXT_POC.md deleted file mode 100644 index af498d7c..00000000 --- a/packages/rstack/SWC_NEXT_POC.md +++ /dev/null @@ -1,30 +0,0 @@ -# SWC next formatter PoC - -This branch replaces the default Yuku parser in `rs fmt` with SWC Next 0.2.2 while retaining Prettier's ESTree printer and the existing formatting adapter. - -`@swc-next/parser@0.2.2` supplies its own platform-specific native binding and decodes its output internally. The rstack native binding is unchanged. - -Inferred JavaScript, JSX, TypeScript, TSX, and embedded script formatting use SWC Next. Explicit `babel` and `typescript` options still select Prettier's own parsers, and project plugins retain precedence. The experimental explicit parser names are `swc-next` and `swc-next-ts`, replacing `yuku` and `yuku-ts`. - -The cache namespace includes the installed parser version so parser upgrades invalidate existing formatting results. The decoder and Rust serializer must be upgraded together; all SWC Next packages in this PoC are locked to 0.2.2. - -## Try it - -```sh -pnpm install -pnpm build -pnpm --filter rstack build:native -printf 'const view=' | pnpm exec rs fmt --stdin-filepath example.tsx -pnpm test -pnpm check -``` - -## Compatibility and limits - -SWC Next 0.2.2 reports diagnostics for function implementations in declaration files, restoring the formatter's existing rejection behavior. It also parses the typed arrow callback inside the conditional expression that previously failed when formatting Rsbuild's `inspectConfig.ts` with 0.2.0. Tests cover both cases, alongside valid declarations, comments, Unicode locations, JSX/TSX, CommonJS, pragmas, and syntax errors. Parser errors stop formatting without falling back to another parser. - -Validation is on macOS arm64 with the repository's Node.js, pnpm, and Rust versions. The complete formatter/CLI suite covers worker execution, stdin, LSP, Vue, Svelte, and caching. Cross-platform binaries and formatter-wide conformance against Prettier's upstream corpus have not been validated. No throughput advantage is claimed by this PoC. - -## Upstream - -[SWC Next 0.2.2 source](https://github.com/swc-project/swc-next/tree/v0.2.2) diff --git a/packages/rstack/rslib.config.ts b/packages/rstack/rslib.config.ts index 2a4ed9e5..bea4163d 100644 --- a/packages/rstack/rslib.config.ts +++ b/packages/rstack/rslib.config.ts @@ -1,13 +1,8 @@ -import { readFileSync } from 'node:fs'; -import { findPackageJSON } from 'node:module'; import { defineConfig } from '@rslib/core'; import prettierPkgJson from 'prettier/package.json' with { type: 'json' }; -import pkgJson from './package.json' with { type: 'json' }; - // @swc-next/parser does not export its package.json. -const swcNextPkgJson = JSON.parse( - readFileSync(findPackageJSON('@swc-next/parser', import.meta.url)!, 'utf8'), -) as { version: string }; +import swcNextPkgJson from './node_modules/@swc-next/parser/package.json' with { type: 'json' }; +import pkgJson from './package.json' with { type: 'json' }; const fullyMinifiedChunks = /(?:fmt(?:Lsp|Plugins)?|sortPackageJsonPlugin|staged)\.js$/; diff --git a/packages/rstack/src/fmt/swcNextPlugin.ts b/packages/rstack/src/fmt/swcNextPlugin.ts index 932fe086..a047ca3c 100644 --- a/packages/rstack/src/fmt/swcNextPlugin.ts +++ b/packages/rstack/src/fmt/swcNextPlugin.ts @@ -1,3 +1,6 @@ +// Adapted from @prettier/plugin-yuku to use SWC Next: +// https://github.com/prettier/prettier/tree/main/packages/plugin-yuku + import * as prettierEstreePlugin from 'prettier/plugins/estree'; import type { Parser, ParserOptions, Plugin } from 'prettier'; import { diff --git a/packages/rstack/tests/fmt/cacheIdentity.test.ts b/packages/rstack/tests/fmt/cacheIdentity.test.ts index cab32674..2da41014 100644 --- a/packages/rstack/tests/fmt/cacheIdentity.test.ts +++ b/packages/rstack/tests/fmt/cacheIdentity.test.ts @@ -1,9 +1,9 @@ -import { readFileSync } from 'node:fs'; -import { findPackageJSON } from 'node:module'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import prettierPkgJson from 'prettier/package.json' with { type: 'json' }; import { expect, test } from 'rstack/test'; +// @swc-next/parser does not export its package.json. +import swcNextPkgJson from '../../node_modules/@swc-next/parser/package.json' with { type: 'json' }; import pkgJson from '../../package.json' with { type: 'json' }; import { cacheHashLength, @@ -86,10 +86,6 @@ test('bypasses user plugins and unserializable options', () => { }); test('includes formatter implementation versions in the namespace', () => { - const swcNextPkgJson = JSON.parse( - readFileSync(findPackageJSON('@swc-next/parser', import.meta.url)!, 'utf8'), - ) as { version: string }; - expect(JSON.parse(cacheNamespace)).toEqual([ fmtCacheVersion, pkgJson.version, From b6d20d3560f7d6b580831a72109f9c2a247abb8c Mon Sep 17 00:00:00 2001 From: sooniter Date: Tue, 22 Sep 2026 12:14:26 +0800 Subject: [PATCH 5/9] refactor(fmt): streamline SWC Next integration --- .github/renovate.json | 4 +-- packages/rstack/rslib.config.ts | 3 -- packages/rstack/src/fmt/cacheIdentity.ts | 2 -- packages/rstack/src/fmt/swcNextParser.ts | 25 -------------- packages/rstack/src/fmt/swcNextPlugin.ts | 33 +++++++++++++------ .../rstack/tests/fmt/cacheIdentity.test.ts | 3 -- pnpm-workspace.yaml | 33 ++----------------- 7 files changed, 27 insertions(+), 76 deletions(-) delete mode 100644 packages/rstack/src/fmt/swcNextParser.ts diff --git a/.github/renovate.json b/.github/renovate.json index 83900773..e334cc23 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -10,8 +10,8 @@ "@rslib/core", "@rslint/core", "@rstest/core", - "prettier", - "yuku-parser" + "@swc-next/parser", + "prettier" ], "groupName": null, "groupSlug": null, diff --git a/packages/rstack/rslib.config.ts b/packages/rstack/rslib.config.ts index bea4163d..2f26a5fc 100644 --- a/packages/rstack/rslib.config.ts +++ b/packages/rstack/rslib.config.ts @@ -1,7 +1,5 @@ import { defineConfig } from '@rslib/core'; import prettierPkgJson from 'prettier/package.json' with { type: 'json' }; -// @swc-next/parser does not export its package.json. -import swcNextPkgJson from './node_modules/@swc-next/parser/package.json' with { type: 'json' }; import pkgJson from './package.json' with { type: 'json' }; const fullyMinifiedChunks = @@ -29,7 +27,6 @@ export default defineConfig({ define: { PRETTIER_VERSION: JSON.stringify(prettierPkgJson.version), RSTACK_VERSION: JSON.stringify(pkgJson.version), - SWC_NEXT_VERSION: JSON.stringify(swcNextPkgJson.version), }, }, output: { diff --git a/packages/rstack/src/fmt/cacheIdentity.ts b/packages/rstack/src/fmt/cacheIdentity.ts index e03d122e..81d7bdd6 100644 --- a/packages/rstack/src/fmt/cacheIdentity.ts +++ b/packages/rstack/src/fmt/cacheIdentity.ts @@ -7,7 +7,6 @@ import type { ResolvedFmtOptions } from './types.ts'; declare const PRETTIER_VERSION: string; declare const RSTACK_VERSION: string; -declare const SWC_NEXT_VERSION: string; type CacheKeyResolver = (filePath: string) => string | undefined; type OptionsHasher = (options: ResolvedFmtOptions) => string | undefined; @@ -22,7 +21,6 @@ const cacheNamespace: string = JSON.stringify([ fmtCacheVersion, RSTACK_VERSION, PRETTIER_VERSION, - SWC_NEXT_VERSION, ]); /** Creates project-relative POSIX cache keys without repeating path setup. */ diff --git a/packages/rstack/src/fmt/swcNextParser.ts b/packages/rstack/src/fmt/swcNextParser.ts deleted file mode 100644 index 6c9633ee..00000000 --- a/packages/rstack/src/fmt/swcNextParser.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - langFromPath, - parseSync as parse, - type ParserOptions, - type ParseResult as SwcParseResult, -} from '@swc-next/parser'; - -export { langFromPath }; -export type { Comment, Diagnostic } from '@swc-next/parser'; -export type Lang = 'js' | 'jsx' | 'ts' | 'tsx' | 'dts'; -export type SourceType = 'module' | 'commonjs'; -export type ParseOptions = { - lang: Lang; - sourceType: SourceType; - preserveParens: true; - comments: 'flat'; -}; - -// Keep the upstream untyped AST behind an unknown boundary. -export type ParseResult = Omit & { - program: unknown; -}; - -export const parseSync = (source: string, options: ParseOptions): ParseResult => - parse(source, options as ParserOptions); diff --git a/packages/rstack/src/fmt/swcNextPlugin.ts b/packages/rstack/src/fmt/swcNextPlugin.ts index a047ca3c..8593e026 100644 --- a/packages/rstack/src/fmt/swcNextPlugin.ts +++ b/packages/rstack/src/fmt/swcNextPlugin.ts @@ -8,11 +8,14 @@ import { parseSync as parseWithSwcNext, type Comment, type Diagnostic, - type ParseOptions, + type ParserOptions as SwcParserOptions, type ParseResult, - type Lang, - type SourceType, -} from './swcNextParser.ts'; + type Lang as SwcLang, +} from '@swc-next/parser'; + +// Use string literals because the package's const enums have no runtime exports. +type Lang = `${SwcLang}`; +type SourceType = 'module' | 'commonjs'; const AST_FORMAT = 'estree-swc-next'; const JS_TS_FILE_REGEXP = /\.(?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$/i; @@ -487,13 +490,13 @@ const createParseError = (error: Diagnostic, text: string): SyntaxError => { const parseWithOptions = ( text: string, - options: Pick, + options: { sourceType: SourceType; lang: Lang }, ): ParseResult => { const result = parseWithSwcNext(text, { preserveParens: true, comments: 'flat', ...options, - }); + } as SwcParserOptions); if (result.diagnostics.length > 0) { throw createParseError(result.diagnostics[0], text); @@ -558,9 +561,14 @@ const parseJavaScript = ( (candidate) => () => parseWithOptions(text, { sourceType: candidate, lang: 'jsx' }), ); - const { program, comments } = tryCombinations(combinations); + const result = tryCombinations(combinations); - return postprocess(asAstNode(program), comments, text, 'swc-next-js'); + return postprocess( + asAstNode(result.program), + result.comments, + text, + 'swc-next-js', + ); }; const parseTypeScript = ( @@ -576,9 +584,14 @@ const parseTypeScript = ( (lang) => () => parseWithOptions(text, { sourceType: candidate, lang }), ), ); - const { program, comments } = tryCombinations(combinations); + const result = tryCombinations(combinations); - return postprocess(asAstNode(program), comments, text, 'swc-next-ts'); + return postprocess( + asAstNode(result.program), + result.comments, + text, + 'swc-next-ts', + ); }; const createParser = ( diff --git a/packages/rstack/tests/fmt/cacheIdentity.test.ts b/packages/rstack/tests/fmt/cacheIdentity.test.ts index 2da41014..29f2bebb 100644 --- a/packages/rstack/tests/fmt/cacheIdentity.test.ts +++ b/packages/rstack/tests/fmt/cacheIdentity.test.ts @@ -2,8 +2,6 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; import prettierPkgJson from 'prettier/package.json' with { type: 'json' }; import { expect, test } from 'rstack/test'; -// @swc-next/parser does not export its package.json. -import swcNextPkgJson from '../../node_modules/@swc-next/parser/package.json' with { type: 'json' }; import pkgJson from '../../package.json' with { type: 'json' }; import { cacheHashLength, @@ -90,7 +88,6 @@ test('includes formatter implementation versions in the namespace', () => { fmtCacheVersion, pkgJson.version, prettierPkgJson.version, - swcNextPkgJson.version, ]); }); diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9dac5e35..5713e7b6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -28,6 +28,7 @@ catalog: '@rstest/adapter-rsbuild': '~0.12.0' '@rstest/adapter-rslib': '~0.12.0' '@rstest/core': '~0.12.0' + '@swc-next/parser': '0.2.2' '@testing-library/dom': '^10.4.2' '@testing-library/jest-dom': '^7.0.1' '@testing-library/react': '^16.3.3' @@ -59,7 +60,6 @@ catalog: 'typescript': '^7.0.2' 'vscode-languageserver': '10.1.1' 'vscode-languageserver-textdocument': '1.0.14' - '@swc-next/parser': '0.2.2' dedupePeers: true @@ -72,36 +72,7 @@ hoistPattern: [] # Reduce the risk of installing compromised packages minimumReleaseAge: 1440 minimumReleaseAgeExclude: - - '@swc-next/parser@0.2.2' - - '@swc-next/decoder@0.2.2' - - '@swc-next/parser-binding-darwin-x64@0.2.2' - - '@swc-next/parser-binding-darwin-arm64@0.2.2' - - '@swc-next/parser-binding-linux-x64-gnu@0.2.2' - - '@swc-next/parser-binding-linux-x64-musl@0.2.2' - - '@swc-next/parser-binding-win32-x64-msvc@0.2.2' - - '@swc-next/parser-binding-linux-arm64-gnu@0.2.2' - - '@swc-next/parser-binding-linux-ppc64-gnu@0.2.2' - - '@swc-next/parser-binding-linux-s390x-gnu@0.2.2' - - '@swc-next/parser-binding-win32-ia32-msvc@0.2.2' - - '@swc-next/parser-binding-linux-arm64-musl@0.2.2' - - '@swc-next/parser-binding-win32-arm64-msvc@0.2.2' - - '@swc-next/parser-binding-linux-riscv64-gnu@0.2.2' - - '@swc-next/parser-binding-linux-riscv64-musl@0.2.2' - - '@yuku-parser/binding-android-arm64@0.8.3' - - '@yuku-parser/binding-darwin-arm64@0.8.3' - - '@yuku-parser/binding-darwin-x64@0.8.3' - - '@yuku-parser/binding-freebsd-x64@0.8.3' - - '@yuku-parser/binding-linux-arm-gnu@0.8.3' - - '@yuku-parser/binding-linux-arm-musl@0.8.3' - - '@yuku-parser/binding-linux-arm64-gnu@0.8.3' - - '@yuku-parser/binding-linux-arm64-musl@0.8.3' - - '@yuku-parser/binding-linux-x64-gnu@0.8.3' - - '@yuku-parser/binding-linux-x64-musl@0.8.3' - - '@yuku-parser/binding-win32-arm64@0.8.3' - - '@yuku-parser/binding-win32-x64@0.8.3' - - '@yuku-toolchain/types@0.8.3' - - 'yuku-ast@0.8.3' - - 'yuku-parser@0.8.3' + - '@swc-next/*' - '@rspack/*' - '@rsbuild/*' - '@rslib/*' From e0a0e82efdd629da0ce3fd8f95cada62fcefeaad Mon Sep 17 00:00:00 2001 From: sooniter Date: Tue, 22 Sep 2026 13:42:04 +0800 Subject: [PATCH 6/9] docs(fmt): describe SWC Next as a high-performance parser --- website/docs/en/guide/formatting.mdx | 2 +- website/docs/zh/guide/formatting.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/en/guide/formatting.mdx b/website/docs/en/guide/formatting.mdx index 24864303..4cd423a8 100644 --- a/website/docs/en/guide/formatting.mdx +++ b/website/docs/en/guide/formatting.mdx @@ -9,7 +9,7 @@ import { PackageManagerTabs } from '@rspress/core/theme'; Rstack CLI includes a formatter built on [Prettier](https://prettier.io/). Compared with running Prettier directly, `rs fmt` includes the following: - **Parallel formatting**: Files are formatted concurrently in a worker pool. -- **SWC Next parser**: The [SWC Next](https://github.com/swc-project/swc-next) parser is used by default for JavaScript, JSX, and TypeScript files. +- **SWC Next parser**: The high-performance [SWC Next](https://github.com/swc-project/swc-next) parser is used by default for JavaScript, JSX, and TypeScript files. - **Persistent cache**: Content-based results let later runs skip formatting unchanged files. `rs fmt` supports Prettier options and plugins and adds built-in capabilities such as [sorting package.json fields](#sort-package-json). diff --git a/website/docs/zh/guide/formatting.mdx b/website/docs/zh/guide/formatting.mdx index 9a743435..dd10d0c6 100644 --- a/website/docs/zh/guide/formatting.mdx +++ b/website/docs/zh/guide/formatting.mdx @@ -9,7 +9,7 @@ import { PackageManagerTabs } from '@rspress/core/theme'; Rstack CLI 提供了基于 [Prettier](https://prettier.io/) 的格式化工具。相比直接使用 Prettier,`rs fmt` 提供了以下能力: - **并行格式化**:通过 worker 池并行格式化文件。 -- **SWC Next 解析器**:默认使用 [SWC Next](https://github.com/swc-project/swc-next) 解析器处理 JavaScript、JSX 和 TypeScript 文件。 +- **SWC Next 解析器**:默认使用高性能的 [SWC Next](https://github.com/swc-project/swc-next) 解析器处理 JavaScript、JSX 和 TypeScript 文件。 - **持久化缓存**:基于文件内容缓存结果,后续运行可以跳过未变化文件的格式化。 `rs fmt` 兼容 Prettier 的选项和插件,并提供更多内置能力,例如支持[排序 package.json 字段](#sort-package-json)。 From 7195815c0471cae530780c2f68d21206b7d03e08 Mon Sep 17 00:00:00 2001 From: sooniter Date: Tue, 22 Sep 2026 13:48:22 +0800 Subject: [PATCH 7/9] refactor(fmt): use SWC Next enum exports directly --- packages/rstack/src/fmt/swcNextPlugin.ts | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/rstack/src/fmt/swcNextPlugin.ts b/packages/rstack/src/fmt/swcNextPlugin.ts index 8593e026..e8e335db 100644 --- a/packages/rstack/src/fmt/swcNextPlugin.ts +++ b/packages/rstack/src/fmt/swcNextPlugin.ts @@ -4,23 +4,23 @@ import * as prettierEstreePlugin from 'prettier/plugins/estree'; import type { Parser, ParserOptions, Plugin } from 'prettier'; import { + CommentMode, + Lang, + SourceType, langFromPath, parseSync as parseWithSwcNext, type Comment, type Diagnostic, - type ParserOptions as SwcParserOptions, type ParseResult, - type Lang as SwcLang, } from '@swc-next/parser'; -// Use string literals because the package's const enums have no runtime exports. -type Lang = `${SwcLang}`; -type SourceType = 'module' | 'commonjs'; - const AST_FORMAT = 'estree-swc-next'; const JS_TS_FILE_REGEXP = /\.(?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$/i; const JSX_REGEXP = /^[^"'`]*<\/|^[^/]{2}.*\/>/m; -const SOURCE_TYPE_COMBINATIONS: SourceType[] = ['module', 'commonjs']; +const SOURCE_TYPE_COMBINATIONS: SourceType[] = [ + SourceType.Module, + SourceType.CommonJs, +]; type Range = [start: number, end: number]; @@ -494,9 +494,9 @@ const parseWithOptions = ( ): ParseResult => { const result = parseWithSwcNext(text, { preserveParens: true, - comments: 'flat', + comments: CommentMode.Flat, ...options, - } as SwcParserOptions); + }); if (result.diagnostics.length > 0) { throw createParseError(result.diagnostics[0], text); @@ -507,11 +507,11 @@ const parseWithOptions = ( const getSourceType = (filepath: string): SourceType | undefined => { if (/\.(?:mjs|mts)$/i.test(filepath)) { - return 'module'; + return SourceType.Module; } if (/\.(?:cjs|cts)$/i.test(filepath)) { - return 'commonjs'; + return SourceType.CommonJs; } return undefined; @@ -525,7 +525,9 @@ const getLanguageCombinations = (text: string, filepath: string): Lang[] => { } // Embedded code from Vue or Svelte keeps the host file path, so detect JSX from its content. - return JSX_REGEXP.test(text) ? ['tsx', 'ts', 'dts'] : ['ts', 'tsx', 'dts']; + return JSX_REGEXP.test(text) + ? [Lang.Tsx, Lang.Ts, Lang.Dts] + : [Lang.Ts, Lang.Tsx, Lang.Dts]; }; const tryCombinations = (combinations: (() => ParseResult)[]): ParseResult => { @@ -559,7 +561,7 @@ const parseJavaScript = ( sourceType ? [sourceType] : SOURCE_TYPE_COMBINATIONS ).map( (candidate) => () => - parseWithOptions(text, { sourceType: candidate, lang: 'jsx' }), + parseWithOptions(text, { sourceType: candidate, lang: Lang.Jsx }), ); const result = tryCombinations(combinations); From 6269c427d8f3de2a74da60b9f61f89b16dea3a33 Mon Sep 17 00:00:00 2001 From: sooniter Date: Tue, 22 Sep 2026 14:04:07 +0800 Subject: [PATCH 8/9] docs(fmt): preserve existing performance description --- website/docs/en/guide/formatting.mdx | 2 +- website/docs/zh/guide/formatting.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/en/guide/formatting.mdx b/website/docs/en/guide/formatting.mdx index 4cd423a8..7708127f 100644 --- a/website/docs/en/guide/formatting.mdx +++ b/website/docs/en/guide/formatting.mdx @@ -6,7 +6,7 @@ description: 'Format files with Rstack CLI using Prettier-compatible options, pl import { PackageManagerTabs } from '@rspress/core/theme'; -Rstack CLI includes a formatter built on [Prettier](https://prettier.io/). Compared with running Prettier directly, `rs fmt` includes the following: +Rstack CLI includes a formatter built on [Prettier](https://prettier.io/). Compared with running Prettier directly, `rs fmt` offers better performance in two ways: - **Parallel formatting**: Files are formatted concurrently in a worker pool. - **SWC Next parser**: The high-performance [SWC Next](https://github.com/swc-project/swc-next) parser is used by default for JavaScript, JSX, and TypeScript files. diff --git a/website/docs/zh/guide/formatting.mdx b/website/docs/zh/guide/formatting.mdx index dd10d0c6..f75ddb75 100644 --- a/website/docs/zh/guide/formatting.mdx +++ b/website/docs/zh/guide/formatting.mdx @@ -6,7 +6,7 @@ description: '使用 Rstack CLI 格式化文件,支持与 Prettier 兼容的 import { PackageManagerTabs } from '@rspress/core/theme'; -Rstack CLI 提供了基于 [Prettier](https://prettier.io/) 的格式化工具。相比直接使用 Prettier,`rs fmt` 提供了以下能力: +Rstack CLI 提供了基于 [Prettier](https://prettier.io/) 的格式化工具。相比直接使用 Prettier,`rs fmt` 的性能更好,主要得益于以下两点: - **并行格式化**:通过 worker 池并行格式化文件。 - **SWC Next 解析器**:默认使用高性能的 [SWC Next](https://github.com/swc-project/swc-next) 解析器处理 JavaScript、JSX 和 TypeScript 文件。 From 889d13edb76328110fbc980ef44fcbfff8521864 Mon Sep 17 00:00:00 2001 From: sooniter Date: Tue, 22 Sep 2026 16:04:25 +0800 Subject: [PATCH 9/9] docs(fmt): link SWC Next parser to npm --- website/docs/en/guide/formatting.mdx | 2 +- website/docs/zh/guide/formatting.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/en/guide/formatting.mdx b/website/docs/en/guide/formatting.mdx index 7708127f..02563027 100644 --- a/website/docs/en/guide/formatting.mdx +++ b/website/docs/en/guide/formatting.mdx @@ -9,7 +9,7 @@ import { PackageManagerTabs } from '@rspress/core/theme'; Rstack CLI includes a formatter built on [Prettier](https://prettier.io/). Compared with running Prettier directly, `rs fmt` offers better performance in two ways: - **Parallel formatting**: Files are formatted concurrently in a worker pool. -- **SWC Next parser**: The high-performance [SWC Next](https://github.com/swc-project/swc-next) parser is used by default for JavaScript, JSX, and TypeScript files. +- **SWC Next parser**: The high-performance [SWC Next](https://www.npmjs.com/package/@swc-next/parser) parser is used by default for JavaScript, JSX, and TypeScript files. - **Persistent cache**: Content-based results let later runs skip formatting unchanged files. `rs fmt` supports Prettier options and plugins and adds built-in capabilities such as [sorting package.json fields](#sort-package-json). diff --git a/website/docs/zh/guide/formatting.mdx b/website/docs/zh/guide/formatting.mdx index f75ddb75..213abed9 100644 --- a/website/docs/zh/guide/formatting.mdx +++ b/website/docs/zh/guide/formatting.mdx @@ -9,7 +9,7 @@ import { PackageManagerTabs } from '@rspress/core/theme'; Rstack CLI 提供了基于 [Prettier](https://prettier.io/) 的格式化工具。相比直接使用 Prettier,`rs fmt` 的性能更好,主要得益于以下两点: - **并行格式化**:通过 worker 池并行格式化文件。 -- **SWC Next 解析器**:默认使用高性能的 [SWC Next](https://github.com/swc-project/swc-next) 解析器处理 JavaScript、JSX 和 TypeScript 文件。 +- **SWC Next 解析器**:默认使用高性能的 [SWC Next](https://www.npmjs.com/package/@swc-next/parser) 解析器处理 JavaScript、JSX 和 TypeScript 文件。 - **持久化缓存**:基于文件内容缓存结果,后续运行可以跳过未变化文件的格式化。 `rs fmt` 兼容 Prettier 的选项和插件,并提供更多内置能力,例如支持[排序 package.json 字段](#sort-package-json)。