diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index c37a99002a..18175ef8c9 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -206,6 +206,9 @@ code unit, matching the original bytes identified by the artifact digest. Plain `.ts` artifact sources use TypeScript syntax without JSX, including angle-bracket type assertions. `.tsx` and `.jsx` retain JSX parsing. +JavaScript sources use JavaScript grammar with JSX, never TypeScript syntax, +so valid JavaScript that TypeScript reads differently keeps its JavaScript +meaning. Relative TypeScript imports can name their emitted `.js`, `.mjs`, or `.cjs` counterparts while resolving to inventoried source files. This substitution diff --git a/src/domain/javascript/javascriptSourceParser.test.ts b/src/domain/javascript/javascriptSourceParser.test.ts new file mode 100644 index 0000000000..e5f829a356 --- /dev/null +++ b/src/domain/javascript/javascriptSourceParser.test.ts @@ -0,0 +1,42 @@ +import { expect, it } from "vitest"; + +import { parseJavaScriptSource } from "./javascriptSourceParser.js"; +import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; + +const analyze = (source: string, path: string) => { + const parsed = parseJavaScriptSource(source, path); + return parsed === null + ? null + : { + errors: parsed.errors.length, + facts: analyzeParsedJavaScriptStaticSource(source, parsed), + }; +}; + +it("parses conditional arrow bodies in JavaScript sources with JavaScript grammar", () => { + // Bundlers emit this shape; TypeScript reads `):H=>` as a return type. + const source = `const {ipcMain}=require("electron"); +const pick=n===0?H=>({start:H.startY}):H=>({start:H.startX}); +ipcMain.handle("layout:ready",()=>pick);`; + for (const path of ["main.js", "main.cjs", "main.mjs", "main.jsx"]) { + const result = analyze(source, path); + expect(result?.errors).toBe(0); + expect(result?.facts.electron.ipc).toContainEqual( + expect.objectContaining({ channel: "layout:ready", operation: "handle" }), + ); + } +}); + +it("keeps JavaScript comparison meaning for angle brackets in JavaScript sources", () => { + const source = `const a=fetch("/api/compare");`; + expect(analyze(source, "main.js")?.facts.endpoints).toEqual([]); + expect(analyze(source, "main.ts")?.facts.endpoints).toContainEqual( + expect.objectContaining({ value: "/api/compare" }), + ); +}); + +it("keeps TypeScript syntax available for TypeScript and unnamed sources", () => { + const source = "let count: number = 1; export type Id = string;"; + expect(parseJavaScriptSource(source, "value.ts")?.errors).toEqual([]); + expect(parseJavaScriptSource(source)?.errors).toEqual([]); +}); diff --git a/src/domain/javascript/javascriptSourceParser.ts b/src/domain/javascript/javascriptSourceParser.ts index 630d5e9bf3..d1141a2434 100644 --- a/src/domain/javascript/javascriptSourceParser.ts +++ b/src/domain/javascript/javascriptSourceParser.ts @@ -9,17 +9,21 @@ export type ParsedJavaScriptSource = ReturnType; * * - `dts:true` only for `.d.ts/.d.mts/.d.cts` ambient declarations; * ordinary `.ts` keeps the missing-initializer diagnostic. - * - JSX enabled for `.tsx`, `.jsx`, `.js`, and unknown paths; disabled for - * plain `.ts/.cts/.mts` (angle-bracket assertions, not JSX) and for all + * - `.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(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-capable contract (best-effort - * parse; failures surface via `errorRecovery`, never silent misparse). + * - 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);