Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(fmt): prototype SWC Next 0.2.2 npm integration
  • Loading branch information
SoonIter committed Sep 22, 2026
commit dae1f3145d3c9f142192f802ccde68a5424c2468
30 changes: 30 additions & 0 deletions packages/rstack/SWC_NEXT_POC.md
Original file line number Diff line number Diff line change
@@ -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=<Component value={{foo:1}} />' | 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)
4 changes: 2 additions & 2 deletions packages/rstack/THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/rstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions packages/rstack/src/fmt/cacheIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
6 changes: 3 additions & 3 deletions packages/rstack/src/fmt/prettierPlugins.ts
Original file line number Diff line number Diff line change
@@ -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<PrettierOptions['plugins']>;

Expand All @@ -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<PrettierPlugins> => {
// 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]
Expand Down
25 changes: 25 additions & 0 deletions packages/rstack/src/fmt/swcNextParser.ts
Original file line number Diff line number Diff line change
@@ -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<SwcParseResult, 'program'> & {
program: unknown;
};

export const parseSync = (source: string, options: ParseOptions): ParseResult =>
parse(source, options as ParserOptions);
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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<ParseOptions, 'sourceType' | 'lang'>,
): ParseResult => {
const result = parseWithSwcNext(text, {
preserveParens: true,
semanticErrors: false,
attachComments: false,
comments: 'flat',
...options,
});

Expand All @@ -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)) {
Expand Down Expand Up @@ -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 = (
Expand All @@ -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 = (
Expand All @@ -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 = (
Expand Down Expand Up @@ -611,23 +610,23 @@ const parseBabel = (text: string, options: ParserOptions<AstNode>): 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 };
1 change: 1 addition & 0 deletions packages/rstack/tests/fmt/cacheIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ test('includes formatter implementation versions in the namespace', () => {
fmtCacheVersion,
pkgJson.version,
prettierPkgJson.version,
'swc-next@0.2.2:npm',
]);
});

Expand Down
Loading
Loading