Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 2 additions & 2 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"@rslib/core",
"@rslint/core",
"@rstest/core",
"prettier",
"yuku-parser"
"@swc-next/parser",
"prettier"
],
"groupName": null,
"groupSlug": null,
Expand Down
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
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
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
// 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 {
CommentMode,
Lang,
SourceType,
langFromPath,
parse as parseWithYuku,
parseSync as parseWithSwcNext,
type Comment,
type Diagnostic,
type ParseOptions,
type ParseResult,
type SourceLang,
type SourceType,
} from 'yuku-parser';
} from '@swc-next/parser';

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'];
const SOURCE_TYPE_COMBINATIONS: SourceType[] = [
SourceType.Module,
SourceType.CommonJs,
];

type Range = [start: number, end: number];

Expand Down Expand Up @@ -158,7 +164,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 +226,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 +362,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 +390,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 +421,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 +488,13 @@ const createParseError = (error: Diagnostic, text: string): SyntaxError => {
);
};

const parseWithOptions = (text: string, options: ParseOptions): ParseResult => {
const result = parseWithYuku(text, {
const parseWithOptions = (
text: string,
options: { sourceType: SourceType; lang: Lang },
): ParseResult => {
const result = parseWithSwcNext(text, {
preserveParens: true,
semanticErrors: false,
attachComments: false,
comments: CommentMode.Flat,
...options,
});

Expand All @@ -499,28 +507,27 @@ const parseWithOptions = (text: string, options: ParseOptions): ParseResult => {

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;
};

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)) {
return [langFromPath(normalizedPath)];
}

// 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 => {
Expand All @@ -542,7 +549,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 @@ -554,11 +561,16 @@ const parseJavaScript = (
sourceType ? [sourceType] : SOURCE_TYPE_COMBINATIONS
).map(
(candidate) => () =>
parseWithOptions(text, { sourceType: candidate, lang: 'jsx' }),
parseWithOptions(text, { sourceType: candidate, lang: Lang.Jsx }),
);
const { program, comments } = tryCombinations(combinations);
const result = tryCombinations(combinations);

return postprocess(program as unknown as AstNode, comments, text, 'yuku-js');
return postprocess(
asAstNode(result.program),
result.comments,
text,
'swc-next-js',
);
};

const parseTypeScript = (
Expand All @@ -574,9 +586,14 @@ const parseTypeScript = (
(lang) => () => parseWithOptions(text, { sourceType: candidate, lang }),
),
);
const { program, comments } = tryCombinations(combinations);
const result = tryCombinations(combinations);

return postprocess(program as unknown as AstNode, comments, text, 'yuku-ts');
return postprocess(
asAstNode(result.program),
result.comments,
text,
'swc-next-ts',
);
};

const createParser = (
Expand Down Expand Up @@ -611,23 +628,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 };
Loading
Loading