Skip to content

Commit 0016242

Browse files
committed
feat(fmt): prototype SWC Next 0.2.0 npm integration
1 parent 0e95ea8 commit 0016242

13 files changed

Lines changed: 337 additions & 228 deletions

packages/rstack/SWC_NEXT_POC.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# SWC next formatter PoC
2+
3+
This branch replaces the default Yuku parser in `rs fmt` with SWC Next 0.2.0 while retaining Prettier's ESTree printer and the existing formatting adapter.
4+
5+
`@swc-next/parser@0.2.0` supplies its own platform-specific native binding and decodes its output internally. The rstack native binding is unchanged.
6+
7+
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`.
8+
9+
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.0.
10+
11+
## Try it
12+
13+
```sh
14+
pnpm install
15+
pnpm build
16+
pnpm --filter rstack build:native
17+
printf 'const view=<Component value={{foo:1}} />' | pnpm exec rs fmt --stdin-filepath example.tsx
18+
pnpm test
19+
pnpm check
20+
```
21+
22+
## Compatibility and limits
23+
24+
SWC Next 0.2.0 accepts function implementations in declaration files, for example `export function value() { return 1; }` in `example.d.ts`. Yuku previously rejected this with an ambient-context diagnostic. The tests explicitly record this upstream difference; valid declarations, comments, Unicode locations, JSX/TSX, CommonJS, pragmas, and syntax errors remain covered. No fallback to another parser hides SWC Next errors.
25+
26+
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.
27+
28+
## Upstream
29+
30+
[SWC Next 0.2.0 source](https://github.com/swc-project/swc-next/tree/v0.2.0)

packages/rstack/THIRD_PARTY_NOTICES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2929
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3030
SOFTWARE.
3131

32-
## Prettier yuku parser adapter
32+
## Prettier SWC next parser adapter
3333

34-
The local Yuku parser adapter includes portions derived from
34+
The local SWC Next parser adapter includes portions derived from
3535
[@prettier/plugin-yuku](https://github.com/prettier/prettier/tree/main/packages/plugin-yuku)
3636
and Prettier's JavaScript parser postprocessing. The adapter reuses the public
3737
ESTree printer, formatter options, and parser utilities from the installed

packages/rstack/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@
7777
"@rslib/core": "catalog:",
7878
"@rslint/core": "catalog:",
7979
"@rstest/core": "catalog:",
80+
"@swc-next/parser": "catalog:",
8081
"prettier": "catalog:",
81-
"tinypool": "catalog:",
82-
"yuku-parser": "catalog:"
82+
"tinypool": "catalog:"
8383
},
8484
"devDependencies": {
8585
"@napi-rs/cli": "catalog:",

packages/rstack/src/fmt/cacheIdentity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const cacheNamespace: string = JSON.stringify([
2121
fmtCacheVersion,
2222
RSTACK_VERSION,
2323
PRETTIER_VERSION,
24+
'swc-next@0.2.0:npm',
2425
]);
2526

2627
/** Creates project-relative POSIX cache keys without repeating path setup. */

packages/rstack/src/fmt/prettierPlugins.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Options as PrettierOptions, Plugin } from 'prettier';
22
import type { ResolvedFmtOptions } from './types.ts';
3-
import { yukuPlugin } from './yukuPlugin.ts';
3+
import { swcNextPlugin } from './swcNextPlugin.ts';
44

55
type PrettierPlugins = NonNullable<PrettierOptions['plugins']>;
66

@@ -15,14 +15,14 @@ const fmtOptionsPlugin = {
1515
},
1616
} satisfies Plugin;
1717

18-
const defaultFmtPlugins: PrettierPlugins = [yukuPlugin, fmtOptionsPlugin];
18+
const defaultFmtPlugins: PrettierPlugins = [swcNextPlugin, fmtOptionsPlugin];
1919

2020
/** Prepends bundled plugins so project plugins can override their parsers. */
2121
const getPrettierPlugins = async (
2222
options: ResolvedFmtOptions,
2323
filePath: string,
2424
): Promise<PrettierPlugins> => {
25-
// An explicit native parser is also the escape hatch for bypassing Yuku.
25+
// An explicit native parser is also the escape hatch for bypassing SWC Next.
2626
const defaultPlugins =
2727
options.parser === 'babel' || options.parser === 'typescript'
2828
? [fmtOptionsPlugin]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {
2+
langFromPath,
3+
parseSync as parse,
4+
type ParserOptions,
5+
type ParseResult as SwcParseResult,
6+
} from '@swc-next/parser';
7+
8+
export { langFromPath };
9+
export type { Comment, Diagnostic } from '@swc-next/parser';
10+
export type Lang = 'js' | 'jsx' | 'ts' | 'tsx' | 'dts';
11+
export type SourceType = 'module' | 'commonjs';
12+
export type ParseOptions = {
13+
lang: Lang;
14+
sourceType: SourceType;
15+
preserveParens: true;
16+
comments: 'flat';
17+
};
18+
19+
// Keep the upstream untyped AST behind an unknown boundary.
20+
export type ParseResult = Omit<SwcParseResult, 'program'> & {
21+
program: unknown;
22+
};
23+
24+
export const parseSync = (source: string, options: ParseOptions): ParseResult =>
25+
parse(source, options as ParserOptions);
Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import * as prettierEstreePlugin from 'prettier/plugins/estree';
22
import type { Parser, ParserOptions, Plugin } from 'prettier';
33
import {
44
langFromPath,
5-
parse as parseWithYuku,
5+
parseSync as parseWithSwcNext,
66
type Comment,
77
type Diagnostic,
88
type ParseOptions,
99
type ParseResult,
10-
type SourceLang,
10+
type Lang,
1111
type SourceType,
12-
} from 'yuku-parser';
12+
} from './swcNextParser.ts';
1313

14-
const AST_FORMAT = 'estree-yuku';
14+
const AST_FORMAT = 'estree-swc-next';
1515
const JS_TS_FILE_REGEXP = /\.(?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$/i;
1616
const JSX_REGEXP = /^[^"'`]*<\/|^[^/]{2}.*\/>/m;
1717
const SOURCE_TYPE_COMBINATIONS: SourceType[] = ['module', 'commonjs'];
@@ -158,7 +158,7 @@ const isAstNode = (value: unknown): value is AstNode =>
158158

159159
const asAstNode = (value: unknown): AstNode => {
160160
if (!isAstNode(value)) {
161-
throw new TypeError('Expected a Yuku AST node.');
161+
throw new TypeError('Expected a SWC Next AST node.');
162162
}
163163
return value;
164164
};
@@ -220,7 +220,7 @@ const stripComments = (
220220
const chunks: string[] = [];
221221
let cursor = 0;
222222

223-
// Yuku returns comments in source order, so mask each range while copying the source only once.
223+
// SWC Next returns comments in source order, so mask each range while copying the source only once.
224224
for (const comment of comments) {
225225
const start = locStart(comment);
226226
const end = locEnd(comment);
@@ -356,7 +356,7 @@ const postprocess = (
356356
ast: AstNode,
357357
comments: PrettierComment[],
358358
text: string,
359-
astType: 'yuku-js' | 'yuku-ts',
359+
astType: 'swc-next-js' | 'swc-next-ts',
360360
): AstNode => {
361361
mergeNestedJsdocComments(comments);
362362

@@ -384,7 +384,7 @@ const postprocess = (
384384
const expression = asAstNode(node.expression);
385385
const start = locStart(node);
386386

387-
// Yuku comments are in source order, so these end offsets are sorted.
387+
// SWC Next comments are in source order, so these end offsets are sorted.
388388
typeCastCommentEnds ??= comments
389389
.filter(isTypeCastComment)
390390
.map((comment) => locEnd(comment));
@@ -415,7 +415,7 @@ const postprocess = (
415415
}
416416

417417
case 'TemplateElement': {
418-
if (astType === 'yuku-ts') {
418+
if (astType === 'swc-next-ts') {
419419
const start = locStart(node) + 1;
420420
const end = locEnd(node) - (node.tail ? 1 : 2);
421421
node.range = [start, end];
@@ -482,11 +482,13 @@ const createParseError = (error: Diagnostic, text: string): SyntaxError => {
482482
);
483483
};
484484

485-
const parseWithOptions = (text: string, options: ParseOptions): ParseResult => {
486-
const result = parseWithYuku(text, {
485+
const parseWithOptions = (
486+
text: string,
487+
options: Pick<ParseOptions, 'sourceType' | 'lang'>,
488+
): ParseResult => {
489+
const result = parseWithSwcNext(text, {
487490
preserveParens: true,
488-
semanticErrors: false,
489-
attachComments: false,
491+
comments: 'flat',
490492
...options,
491493
});
492494

@@ -509,10 +511,7 @@ const getSourceType = (filepath: string): SourceType | undefined => {
509511
return undefined;
510512
};
511513

512-
const getLanguageCombinations = (
513-
text: string,
514-
filepath: string,
515-
): SourceLang[] => {
514+
const getLanguageCombinations = (text: string, filepath: string): Lang[] => {
516515
const normalizedPath = filepath.toLowerCase();
517516

518517
if (JS_TS_FILE_REGEXP.test(normalizedPath)) {
@@ -542,7 +541,7 @@ const tryCombinations = (combinations: (() => ParseResult)[]): ParseResult => {
542541
throw firstError;
543542
}
544543

545-
throw new Error('No Yuku parser combinations were provided.');
544+
throw new Error('No SWC Next parser combinations were provided.');
546545
};
547546

548547
const parseJavaScript = (
@@ -558,7 +557,7 @@ const parseJavaScript = (
558557
);
559558
const { program, comments } = tryCombinations(combinations);
560559

561-
return postprocess(program as unknown as AstNode, comments, text, 'yuku-js');
560+
return postprocess(asAstNode(program), comments, text, 'swc-next-js');
562561
};
563562

564563
const parseTypeScript = (
@@ -576,7 +575,7 @@ const parseTypeScript = (
576575
);
577576
const { program, comments } = tryCombinations(combinations);
578577

579-
return postprocess(program as unknown as AstNode, comments, text, 'yuku-ts');
578+
return postprocess(asAstNode(program), comments, text, 'swc-next-ts');
580579
};
581580

582581
const createParser = (
@@ -611,23 +610,23 @@ const parseBabel = (text: string, options: ParserOptions<AstNode>): AstNode => {
611610
};
612611
};
613612

614-
const yukuParser = createParser(parseJavaScript);
615-
const yukuBabelParser = createParser(parseBabel);
616-
const yukuTypeScriptParser = createParser(parseTypeScript);
613+
const swcNextParser = createParser(parseJavaScript);
614+
const swcNextBabelParser = createParser(parseBabel);
615+
const swcNextTypeScriptParser = createParser(parseTypeScript);
617616

618-
const yukuPlugin: Plugin = {
617+
const swcNextPlugin: Plugin = {
619618
options: estreePlugin.options,
620619
parsers: {
621620
// Prettier resolves parsers from the last plugin that provides the name.
622621
// Project plugins are loaded after this one, so parser wrappers take priority.
623-
babel: yukuBabelParser,
624-
typescript: yukuTypeScriptParser,
625-
yuku: yukuParser,
626-
'yuku-ts': yukuTypeScriptParser,
622+
babel: swcNextBabelParser,
623+
typescript: swcNextTypeScriptParser,
624+
'swc-next': swcNextParser,
625+
'swc-next-ts': swcNextTypeScriptParser,
627626
},
628627
printers: {
629628
[AST_FORMAT]: estreePrinter,
630629
},
631630
};
632631

633-
export { yukuPlugin };
632+
export { swcNextPlugin };

packages/rstack/tests/fmt/cacheIdentity.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ test('includes formatter implementation versions in the namespace', () => {
8888
fmtCacheVersion,
8989
pkgJson.version,
9090
prettierPkgJson.version,
91+
'swc-next@0.2.0:npm',
9192
]);
9293
});
9394

0 commit comments

Comments
 (0)