mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 02:15:44 +00:00
parserPluginsForPath enabled Babel's typescript plugin for .js, .jsx, .mjs and .cjs sources. TypeScript reads some valid JavaScript differently: a conditional whose consequent is an arrow with a parenthesized body (c?x=>(a):y=>(b)) throws as a return-type annotation, and a<b>(c) becomes a generic call. VS Code's 20 MB workbench bundles therefore returned no AST at all. Select JavaScript grammar (JSX plus legacy decorators) for explicit JavaScript extensions. TypeScript dialects and unnamed sources keep the existing TypeScript-capable contract. Fixes #3780 Refs #3670 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
5.5 KiB
TypeScript
148 lines
5.5 KiB
TypeScript
import { parse, type ParserOptions, type ParserPlugin } from "@babel/parser";
|
|
|
|
/** Babel AST produced by REA's inert JavaScript parser boundary. */
|
|
export type ParsedJavaScriptSource = ReturnType<typeof parse>;
|
|
|
|
/**
|
|
* Babel plugins for one source path. Single owner for TypeScript/JSX
|
|
* mode selection (tsc + Babel `typescript` plugin semantics).
|
|
*
|
|
* - `dts:true` only for `.d.ts/.d.mts/.d.cts` ambient declarations;
|
|
* ordinary `.ts` keeps the missing-initializer diagnostic.
|
|
* - `.js/.jsx/.mjs/.cjs` use JavaScript grammar with JSX. TypeScript reads
|
|
* some valid JavaScript differently (`c?x=>(a):y=>(b)` as a return type,
|
|
* `a<b>(c)` as a generic call), so it must not select their meaning.
|
|
* - JSX enabled for `.tsx` and unknown paths; disabled for plain
|
|
* `.ts/.cts/.mts` (angle-bracket assertions, not JSX) and for all
|
|
* declaration files. `.mts` (including `.d.mts`) sets
|
|
* `disallowAmbiguousJSXLike`, matching tsc's `.mts` mode.
|
|
* - `decorators-legacy` always present: without it decorated sources fail
|
|
* hard instead of reporting recovered syntax, losing every derived fact.
|
|
* - Unknown paths retain the existing JSX- and TypeScript-capable contract
|
|
* (best-effort parse; failures surface via `errorRecovery`).
|
|
*/
|
|
export const parserPluginsForPath = (sourcePath?: string): ParserPlugin[] => {
|
|
const path = sourcePath ?? "";
|
|
if (/\.(?:js|jsx|mjs|cjs)$/iu.test(path)) return ["decorators-legacy", "jsx"];
|
|
const dts = /\.d\.(?:ts|mts|cts)$/iu.test(path);
|
|
const isTsLike = /\.(?:ts|cts|mts)$/iu.test(path);
|
|
const isTsx = /\.tsx$/iu.test(path);
|
|
const isMts = /\.mts$/iu.test(path);
|
|
const typescript: ParserPlugin = [
|
|
"typescript",
|
|
{
|
|
dts,
|
|
...(isMts ? { disallowAmbiguousJSXLike: true } : {}),
|
|
},
|
|
];
|
|
const jsxCapable = !dts && (isTsx || !isTsLike);
|
|
return jsxCapable
|
|
? ["decorators-legacy", "jsx", typescript]
|
|
: ["decorators-legacy", typescript];
|
|
};
|
|
|
|
/** `.mjs` and `.mts` explicitly select module mode. */
|
|
export const isExplicitModuleSourcePath = (path?: string): boolean =>
|
|
/\.m[jt]s$/iu.test(path ?? "");
|
|
|
|
const runtimeSourceTypeForPath = (
|
|
path?: string,
|
|
): "commonjs" | "module" | "unambiguous" =>
|
|
/\.cjs$/iu.test(path ?? "")
|
|
? "commonjs"
|
|
: isExplicitModuleSourcePath(path)
|
|
? "module"
|
|
: "unambiguous";
|
|
|
|
export interface RecoveredJavaScriptParse {
|
|
readonly file: ParsedJavaScriptSource;
|
|
/** Errors that apply only if a source of unknown mode is an ES module. */
|
|
readonly moduleOnlyErrors: readonly string[];
|
|
}
|
|
|
|
/**
|
|
* Parse a module or script with recovery. Babel tries module mode first and
|
|
* keeps its strict-mode errors after falling back to a script, so a script
|
|
* with errors is parsed again as a script. Strict errors that only the module
|
|
* parse reports stay visible because the source may still be a module.
|
|
*/
|
|
export const parseRecoveredJavaScript = (
|
|
source: string,
|
|
sourcePath: string | undefined,
|
|
options: Omit<ParserOptions, "sourceType" | "errorRecovery">,
|
|
): RecoveredJavaScriptParse => {
|
|
const sourceType = runtimeSourceTypeForPath(sourcePath);
|
|
if (sourceType !== "unambiguous") {
|
|
const normalizedOptions = { ...options };
|
|
if (sourceType === "commonjs") {
|
|
// Babel owns these wrapper semantics in CommonJS mode and rejects
|
|
// explicit script overrides, including the reference-import options.
|
|
delete normalizedOptions.allowAwaitOutsideFunction;
|
|
delete normalizedOptions.allowReturnOutsideFunction;
|
|
delete normalizedOptions.allowNewTargetOutsideFunction;
|
|
}
|
|
return {
|
|
file: parse(source, {
|
|
...normalizedOptions,
|
|
sourceType,
|
|
errorRecovery: true,
|
|
}),
|
|
moduleOnlyErrors: [],
|
|
};
|
|
}
|
|
const file = parse(source, {
|
|
...options,
|
|
sourceType: "unambiguous",
|
|
errorRecovery: true,
|
|
});
|
|
if (file.program.sourceType !== "script" || file.errors.length === 0)
|
|
return { file, moduleOnlyErrors: [] };
|
|
const script = parse(source, {
|
|
...options,
|
|
sourceType: "script",
|
|
errorRecovery: true,
|
|
});
|
|
const scriptErrors = new Set(script.errors.map((error) => error.message));
|
|
return {
|
|
file: script,
|
|
moduleOnlyErrors: file.errors
|
|
.map((error) => error.message)
|
|
.filter((message) => !scriptErrors.has(message)),
|
|
};
|
|
};
|
|
|
|
/** Parse inert JavaScript or TypeScript facts without attaching comments to AST nodes. */
|
|
export const parseJavaScriptSource = (
|
|
source: string,
|
|
sourcePath?: string,
|
|
): ParsedJavaScriptSource | null => {
|
|
try {
|
|
const runtimeSourceType = runtimeSourceTypeForPath(sourcePath);
|
|
const commonjs = runtimeSourceType === "commonjs";
|
|
const options = {
|
|
errorRecovery: true,
|
|
attachComment: false,
|
|
plugins: parserPluginsForPath(sourcePath),
|
|
};
|
|
const file = parse(source, {
|
|
...options,
|
|
// Inert artifact analysis retains module declarations even in .cjs assets.
|
|
// Infer their syntax here; reference imports keep filename-mode diagnostics.
|
|
sourceType: commonjs ? "unambiguous" : runtimeSourceType,
|
|
allowReturnOutsideFunction: commonjs,
|
|
allowNewTargetOutsideFunction: commonjs,
|
|
});
|
|
// Unambiguous mode retains module-only diagnostics after choosing a script.
|
|
// The .cjs extension establishes wrapper mode, so obtain its own diagnostics.
|
|
return commonjs &&
|
|
file.program.sourceType === "script" &&
|
|
file.errors.length > 0
|
|
? parse(source, { ...options, sourceType: "commonjs" })
|
|
: file;
|
|
} catch (cause: unknown) {
|
|
// Unparseable source is represented by the null return.
|
|
void cause;
|
|
return null;
|
|
}
|
|
};
|