fix(javascript): parse explicit CommonJS wrapper syntax (#2475)
This commit is contained in:
parent
e74e4e951b
commit
6a04f922e1
|
|
@ -37,10 +37,19 @@ export const parserPluginsForPath = (sourcePath?: string): ParserPlugin[] => {
|
|||
: ["decorators-legacy", typescript];
|
||||
};
|
||||
|
||||
/** `.mjs` and `.mts` are modules by extension; other paths leave the mode to syntax. */
|
||||
/** `.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. */
|
||||
|
|
@ -58,25 +67,25 @@ export const parseRecoveredJavaScript = (
|
|||
sourcePath: string | undefined,
|
||||
options: Omit<ParserOptions, "sourceType" | "errorRecovery">,
|
||||
): RecoveredJavaScriptParse => {
|
||||
// `.cjs` is CommonJS by extension, so module-only errors cannot apply.
|
||||
if (/\.cjs$/iu.test(sourcePath ?? ""))
|
||||
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, {
|
||||
...options,
|
||||
sourceType: "script",
|
||||
errorRecovery: true,
|
||||
}),
|
||||
moduleOnlyErrors: [],
|
||||
};
|
||||
if (isExplicitModuleSourcePath(sourcePath))
|
||||
return {
|
||||
file: parse(source, {
|
||||
...options,
|
||||
sourceType: "module",
|
||||
...normalizedOptions,
|
||||
sourceType,
|
||||
errorRecovery: true,
|
||||
}),
|
||||
moduleOnlyErrors: [],
|
||||
};
|
||||
}
|
||||
const file = parse(source, {
|
||||
...options,
|
||||
sourceType: "unambiguous",
|
||||
|
|
@ -98,18 +107,34 @@ export const parseRecoveredJavaScript = (
|
|||
};
|
||||
};
|
||||
|
||||
/** Parse JavaScript or TypeScript once without attaching comments to AST nodes. */
|
||||
/** Parse inert JavaScript or TypeScript facts without attaching comments to AST nodes. */
|
||||
export const parseJavaScriptSource = (
|
||||
source: string,
|
||||
sourcePath?: string,
|
||||
): ParsedJavaScriptSource | null => {
|
||||
try {
|
||||
return parse(source, {
|
||||
sourceType: "unambiguous",
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,31 @@ import { expect, it } from "vitest";
|
|||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { importReferenceSource } from "../../support/referenceSourceResourceScope.js";
|
||||
|
||||
it("imports valid CommonJS wrapper syntax without inventing a parse failure", async () => {
|
||||
const root = await createTestTempDirectory("rea-reference-commonjs-mode-");
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(root, "main.cjs"),
|
||||
'void new.target; require("./dep.cjs"); return;\n',
|
||||
),
|
||||
writeFile(join(root, "dep.cjs"), "module.exports = 1;\n"),
|
||||
]);
|
||||
const imported = await importReferenceSource({
|
||||
root,
|
||||
caller: "native-commonjs-source",
|
||||
policy: { secretPatterns: [] },
|
||||
});
|
||||
if (!imported.ok) throw imported.error;
|
||||
expect(imported.value.parse_failures).toEqual([]);
|
||||
expect(imported.value.relationships).toContainEqual({
|
||||
from_path: "main.cjs",
|
||||
to: "dep.cjs",
|
||||
kind: "requires",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["main.ts", "dep.js", "dep.ts"],
|
||||
["main.mts", "dep.mjs", "dep.mts"],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { expect } from "vitest";
|
||||
|
||||
import { parseJavaScriptSource } from "../../../src/domain/javascript/javascriptSourceParser.js";
|
||||
import { processTest } from "../../support/process/processFixture.js";
|
||||
|
||||
for (const source of [
|
||||
"return;",
|
||||
"void new.target;",
|
||||
"var await = 1; return;",
|
||||
"with ({ value: 1 }) { void value; } void new.target;",
|
||||
]) {
|
||||
processTest(
|
||||
`accepts Node CommonJS syntax while retaining module errors: ${source}`,
|
||||
async ({ processes }) => {
|
||||
const commonjs = await processes.run(
|
||||
process.execPath,
|
||||
["--check", "--input-type=commonjs"],
|
||||
{ input: source, timeoutMs: 5_000 },
|
||||
);
|
||||
expect(commonjs.exitCode).toBe(0);
|
||||
const module = await processes.run(
|
||||
process.execPath,
|
||||
["--check", "--input-type=module"],
|
||||
{ input: source, timeoutMs: 5_000 },
|
||||
);
|
||||
expect(module.exitCode).not.toBe(0);
|
||||
|
||||
const parsed = parseJavaScriptSource(source, "main.cjs");
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed?.errors).toEqual([]);
|
||||
expect(
|
||||
parseJavaScriptSource(source, "main.mjs")?.errors.length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
parseJavaScriptSource(source, "main.mts")?.errors.length,
|
||||
).toBeGreaterThan(0);
|
||||
},
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue