From e2da92f5d233f914b9677f2db14ba994933f47f3 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:23:15 +0800 Subject: [PATCH] fix(javascript): unify runtime module declarations --- src/browser/WebSourceMapFetcher.ts | 18 +++------- .../javascript/javascriptModuleDeclaration.ts | 11 ++++++ .../javascript/javascriptStaticAnalysis.ts | 35 ++++++------------- src/domain/webBundleAnalyzer.test.ts | 24 +++++++++++++ src/domain/webBundleAnalyzerInspection.ts | 23 +++++------- src/domain/webModuleImports.ts | 23 ++++-------- 6 files changed, 65 insertions(+), 69 deletions(-) diff --git a/src/browser/WebSourceMapFetcher.ts b/src/browser/WebSourceMapFetcher.ts index b5264265a2..d71f3a9a26 100644 --- a/src/browser/WebSourceMapFetcher.ts +++ b/src/browser/WebSourceMapFetcher.ts @@ -17,8 +17,7 @@ import { analyzeParsedJavaScriptReferences } from "../domain/javascript/javascri import { traverseJavaScriptAst } from "../domain/javascript/javascriptSemanticTraversal.js"; import { parseJavaScriptSource } from "../domain/javascript/javascriptSourceParser.js"; import { - isTypeOnlyExportDeclaration, - isTypeOnlyImportDeclaration, + semanticStaticModuleSource, semanticImportEqualsSpecifier, } from "../domain/javascript/javascriptModuleDeclaration.js"; import { stringValue } from "../domain/javascript/javascriptStaticAnalysisHelpers.js"; @@ -1234,18 +1233,9 @@ const originalDependency = ( readonly specifier: string; readonly requireCallee?: t.Identifier; } | null => { - if ( - t.isImportDeclaration(node) || - t.isExportAllDeclaration(node) || - t.isExportNamedDeclaration(node) - ) - return node.source === null || - node.source === undefined || - (t.isImportDeclaration(node) - ? isTypeOnlyImportDeclaration(node) - : isTypeOnlyExportDeclaration(node)) - ? null - : { kind: "static_import", specifier: node.source.value }; + const moduleSource = semanticStaticModuleSource(node); + if (moduleSource !== null) + return { kind: "static_import", specifier: moduleSource.value }; if (t.isImportExpression(node)) { const specifier = stringValue(node.source); return specifier === undefined diff --git a/src/domain/javascript/javascriptModuleDeclaration.ts b/src/domain/javascript/javascriptModuleDeclaration.ts index b071139030..0bf60a2499 100644 --- a/src/domain/javascript/javascriptModuleDeclaration.ts +++ b/src/domain/javascript/javascriptModuleDeclaration.ts @@ -30,6 +30,17 @@ export const isTypeOnlyExportDeclaration = ( node.declaration != null && isErasedJavaScriptDeclaration(node.declaration)); +/** Runtime static module source, shared by source and captured-script consumers. */ +export const semanticStaticModuleSource = ( + node: t.Node, +): t.StringLiteral | null => { + if (t.isImportDeclaration(node)) + return isTypeOnlyImportDeclaration(node) ? null : node.source; + if (t.isExportAllDeclaration(node) || t.isExportNamedDeclaration(node)) + return isTypeOnlyExportDeclaration(node) ? null : (node.source ?? null); + return null; +}; + /** * Runtime external-module target for a value import-equals declaration; * `import type X = require("m")` is erased and declares no dependency. diff --git a/src/domain/javascript/javascriptStaticAnalysis.ts b/src/domain/javascript/javascriptStaticAnalysis.ts index 416eecf3c3..879caf5ea5 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.ts @@ -72,8 +72,7 @@ import { import { createJavaScriptRpcScan } from "./javascriptRpcEndpoints.js"; import { hasUncertainTypeOnlyModuleSideEffect, - isTypeOnlyExportDeclaration, - isTypeOnlyImportDeclaration, + semanticStaticModuleSource, semanticImportEqualsSpecifier, } from "./javascriptModuleDeclaration.js"; import { @@ -367,28 +366,16 @@ const inspectNode = ( const specifier = semanticImportEqualsSpecifier(node); if (specifier !== null) addReference(findings, { node, kind: "require", specifier }); - } else if ( - (t.isImportDeclaration(node) || t.isExportAllDeclaration(node)) && - node.source !== undefined && - !(t.isImportDeclaration(node) && isTypeOnlyImportDeclaration(node)) && - !(t.isExportAllDeclaration(node) && isTypeOnlyExportDeclaration(node)) - ) - addReference(findings, { - node, - kind: "static-import", - specifier: node.source.value, - }); - else if ( - t.isExportNamedDeclaration(node) && - node.source != null && - !isTypeOnlyExportDeclaration(node) - ) - addReference(findings, { - node, - kind: "static-import", - specifier: node.source.value, - }); - else if (t.isImportExpression(node)) + } else { + const moduleSource = semanticStaticModuleSource(node); + if (moduleSource !== null) + addReference(findings, { + node, + kind: "static-import", + specifier: moduleSource.value, + }); + } + if (t.isImportExpression(node)) addReference(findings, { node, kind: "dynamic-import", diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index 787ef2ca78..f08d7ae758 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -16,6 +16,30 @@ import { createWebTextArtifact } from "./webContentArtifact.js"; const origin = "https://app.example.test"; +it("shares runtime module declarations with source analysis while retaining inline type side effects", () => { + const source = ` + import type {T} from "./erased-import"; + export type {T} from "./erased-export"; + export type * from "./erased-all"; + import {type X} from "./inline-import"; + export {type Y} from "./inline-export"; + import ""; + export * from "./runtime"; + export const local = 1; + `; + const expected = ["./inline-import", "./inline-export", "", "./runtime"]; + const result = analyzeWebBundleFixture(inspection(source)); + expect(result.completeness.parse_failures).toBe(0); + expect( + result.observations.chunks.edges.map(({ specifier }) => specifier), + ).toEqual(expected); + expect( + analyzeJavaScriptStaticSource(source) + .references.filter(({ kind }) => kind === "static-import") + .map(({ specifier }) => specifier), + ).toEqual(expected); +}); + it("retains standalone GraphQL Source operations in captured scripts", () => { const result = analyzeWebBundleFixture( inspection(` diff --git a/src/domain/webBundleAnalyzerInspection.ts b/src/domain/webBundleAnalyzerInspection.ts index 09425f4bc5..7faeb18574 100644 --- a/src/domain/webBundleAnalyzerInspection.ts +++ b/src/domain/webBundleAnalyzerInspection.ts @@ -9,9 +9,11 @@ import type { JavaScriptRouteSourceFacts } from "./javascript/javascriptRouteFac import { sanitizeEndpointCandidate } from "./browserObservation.js"; import { compositeKey } from "./unicodeCodePointOrder.js"; import type { WebBundleAnalysis } from "./webBundleAnalysis.js"; -import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js"; +import { + traverseJavaScriptAst, + completeSemanticSteps, +} from "./javascript/javascriptSemanticTraversal.js"; import { indexJavaScriptBindingsWithReceiversSteps } from "./javascript/javascriptSemanticAnalysis.js"; -import { completeSemanticSteps } from "./javascript/javascriptSemanticTraversal.js"; import type { JavaScriptOpenReceiverFact } from "./javascript/javascriptSemanticAnalysis.js"; import { isIntrinsicMapLookup } from "./javascript/javascriptSemanticMapOperations.js"; import { @@ -36,6 +38,7 @@ import { isUrlLikeModuleSpecifier, } from "./webBundleAnalyzerAst.js"; import { createJavaScriptRpcScan } from "./javascript/javascriptRpcEndpoints.js"; +import { semanticStaticModuleSource } from "./javascript/javascriptModuleDeclaration.js"; import { parseJavaScriptSource } from "./javascript/javascriptSourceParser.js"; type BundleObservations = WebBundleAnalysis["observations"]; @@ -132,21 +135,11 @@ const inspectNode = ( node: t.CallExpression | t.OptionalCallExpression | t.NewExpression, ) => boolean, ): void => { - if ( - (t.isImportDeclaration(node) || t.isExportAllDeclaration(node)) && - node.source !== undefined - ) + const moduleSource = semanticStaticModuleSource(node); + if (moduleSource !== null) addEdge({ script, - specifier: node.source.value, - kind: "static_import", - node, - accumulator, - }); - else if (t.isExportNamedDeclaration(node) && node.source != null) - addEdge({ - script, - specifier: node.source.value, + specifier: moduleSource.value, kind: "static_import", node, accumulator, diff --git a/src/domain/webModuleImports.ts b/src/domain/webModuleImports.ts index c94b98e426..a8162030ee 100644 --- a/src/domain/webModuleImports.ts +++ b/src/domain/webModuleImports.ts @@ -6,8 +6,7 @@ import type { WebModuleImport } from "./webModuleTrace.js"; import type { ExportedWebScript } from "./webScriptExport.js"; import { hasUncertainTypeOnlyModuleSideEffect, - isTypeOnlyExportDeclaration, - isTypeOnlyImportDeclaration, + semanticStaticModuleSource, } from "./javascript/javascriptModuleDeclaration.js"; /** Collect module declarations, omitting whole-statement type-only forms. */ @@ -60,20 +59,12 @@ export const collectWebModuleImports = ( }; traverseJavaScriptAst(ast, { enter: (node) => { - if (node.type === "ImportDeclaration") { - if (isTypeOnlyImportDeclaration(node)) return; - add("static-import", node.source); - } else if ( - node.type === "ExportAllDeclaration" || - node.type === "ExportNamedDeclaration" - ) { - if ( - isTypeOnlyExportDeclaration(node) || - node.source === null || - node.source === undefined - ) - return; - add("re-export", node.source); + const moduleSource = semanticStaticModuleSource(node); + if (moduleSource !== null) { + add( + node.type === "ImportDeclaration" ? "static-import" : "re-export", + moduleSource, + ); } else if (node.type === "ImportExpression") add("dynamic-import", node.source); },