diff --git a/src/domain/javascript/javascriptSemanticAnalysis.require_links.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.require_links.test.ts index eafb40255b..675234dec6 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.require_links.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.require_links.test.ts @@ -160,6 +160,11 @@ describe("TypeScript type-only module links", () => { export type { hiddenFunction }; `); + expect(ir.bindings.map(({ name }) => name).sort()).toEqual([ + "hiddenFunction", + "value", + ]); + const externalLinks = ir.moduleLinks .filter(({ specifier }) => specifier !== null) .map( diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index 56f72c8122..a2c0384af1 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -80,6 +80,8 @@ import { } from "./javascriptSemanticAmbientWrites.js"; import { hasUncertainTypeOnlyModuleSideEffect, + isTypeOnlyImportDeclaration, + isTypeOnlyImportSpecifier, semanticImportEqualsSpecifier, } from "./javascriptModuleDeclaration.js"; import { @@ -2038,7 +2040,10 @@ const bindImports = ( scope: JavaScriptSemanticScopeState, state: JavaScriptSemanticAnalysisState, ): void => { + if (isTypeOnlyImportDeclaration(node)) return; for (const specifier of node.specifiers) { + if (t.isImportSpecifier(specifier) && isTypeOnlyImportSpecifier(specifier)) + continue; const importedPath = t.isImportDefaultSpecifier(specifier) ? ["default"] : t.isImportNamespaceSpecifier(specifier) @@ -2053,12 +2058,7 @@ const bindImports = ( definitionNode: specifier.local, initializer: null, directOrigin: { specifier: node.source.value, importedPath }, - runtimeImport: - node.importKind !== "type" && - node.importKind !== "typeof" && - (!t.isImportSpecifier(specifier) || - (specifier.importKind !== "type" && - specifier.importKind !== "typeof")), + runtimeImport: true, }); } }; diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index 38d10314f1..73dcbca690 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -1192,16 +1192,7 @@ const provenanceForBindingInitializers = ( : "Catch binding provenance depends on the thrown value.", ); if (binding.directOrigins.length > 0) - return binding.kind === "import" && - !binding.runtimeImport && - binding.directOrigins.every( - ({ resolutionMode }) => resolutionMode !== "require", - ) - ? semanticAmbiguousProvenance( - binding.directOrigins, - "A type-only import does not establish a runtime module value.", - ) - : semanticOriginsProvenance(binding.directOrigins); + return semanticOriginsProvenance(binding.directOrigins); if (binding.initializers.length === 0) return semanticLocalProvenance(); if (binding.initializers.length > 1) return semanticAmbiguousProvenance( diff --git a/tests/acceptance/applications/javascriptWorkerUrlCli.test.ts b/tests/acceptance/applications/javascriptWorkerUrlCli.test.ts index 1039f5d3a1..a029f003e2 100644 --- a/tests/acceptance/applications/javascriptWorkerUrlCli.test.ts +++ b/tests/acceptance/applications/javascriptWorkerUrlCli.test.ts @@ -1,4 +1,7 @@ -import { expect } from "vitest"; +import { runInNewContext } from "node:vm"; + +import { transformSync } from "esbuild"; +import { expect, it } from "vitest"; import type { JavaScriptApplicationGraph } from "../../../src/domain/javascript/javascriptApplicationGraph.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -6,6 +9,37 @@ import { cliTest } from "../../support/cli/cliFixture.js"; import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js"; import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js"; +const typeOnlyWorkerImports = [ + 'import type { Worker } from "node:worker_threads";', + 'import { type Worker } from "node:worker_threads";', +]; + +it.each(typeOnlyWorkerImports)( + "preserves the ambient Worker at runtime after erasing %s", + (prefix) => { + const workerUrls: string[] = []; + const { code } = transformSync( + `${prefix}\nnew Worker(new URL("./worker.js", import.meta.url));`, + { + loader: "ts", + format: "iife", + define: { + "import.meta.url": JSON.stringify("file:///fixture/main.ts"), + }, + }, + ); + runInNewContext(code, { + URL, + Worker: class { + constructor(url: URL) { + workerUrls.push(url.href); + } + }, + }); + expect(workerUrls).toEqual(["file:///fixture/worker.js"]); + }, +); + const workerReferences = (graph: JavaScriptApplicationGraph) => graph.nodes .filter(({ kind }) => kind === "worker") @@ -84,6 +118,12 @@ cliTest( suffix: "", resolves: true, }, + ...typeOnlyWorkerImports.map((prefix) => ({ + prefix, + constructor: "new Worker", + suffix: "", + resolves: true, + })), ...[ ["class Worker {}", "new Worker", ""], ["globalThis.Worker = Fake;", "new Worker", ""], @@ -94,16 +134,6 @@ cliTest( ["const custom = { Worker: class {} };", "new custom.Worker", ""], ["function start(window) {", "new window.Worker", "}"], ["function start(globalThis) {", "new globalThis.Worker", "}"], - [ - 'import type { Worker } from "node:worker_threads";', - "new Worker", - "", - ], - [ - 'import { type Worker } from "node:worker_threads";', - "new Worker", - "", - ], [ 'import type * as threads from "node:worker_threads";', "new threads.Worker",