Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
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.0 while retaining Prettier's ESTree printer and the existing formatting adapter.

`@swc-next/parser@0.2.0` 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.0.

## 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.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.

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.0 source](https://github.com/swc-project/swc-next/tree/v0.2.0)
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.0: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.0:npm',
]);
});

Expand Down
Loading