From 1c90ad084441d32472dbba7bf13ee44c83c1368d Mon Sep 17 00:00:00 2001 From: Max B Date: Sun, 23 Aug 2026 21:45:17 +0200 Subject: [PATCH] fix(dynamic): stub non-compilable dynamic imports to Promise.resolve({}) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related gaps in --dynamic mode: 1. .tsx files (JSX): `dynamicImportProgramTargetOf` now returns null for .tsx files, keeping them out of the compiled module graph. The `lowerOwnModuleImport` path then stubs the import to `Promise.resolve({})` via the new `dep !== null && L.dynamic` branch. 2. Program modules with no builder: when a module IS in the checker's program (dep !== null) but has no compilation story — no %init was produced, e.g. a module that was explicitly excluded from the compiled graph — the compiler previously emitted SC1090 unconditionally. In --dynamic mode, this now stubs to `Promise.resolve({})` instead. Callers that destructure the namespace receive `undefined` for every export, which is the correct behavior when the module is not reachable from the native binary. These two changes together cover the common --dynamic pattern of lazy module loading where some targets are intentionally not compiled natively (JSX widgets, server-only modules). --- .../compiler/src/frontend/lowering/lower-island.ts | 13 +++++++++++++ .../compiler/src/frontend/lowering/lower-modules.ts | 3 +++ 2 files changed, 16 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/lower-island.ts b/packages/compiler/src/frontend/lowering/lower-island.ts index 467490f2b..35e266145 100644 --- a/packages/compiler/src/frontend/lowering/lower-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-island.ts @@ -2832,6 +2832,19 @@ export function lowerStaticReadableStreamReaderCall( } const builder = dep !== null ? dynNsBuilderOf(L, dep, loc) : null; if (builder === null) { + // In --dynamic mode, a module that is in the program graph but has no + // compilation story (excluded from the compiled module graph, or contains + // features the native compiler cannot lower) resolves to an empty namespace + // stub rather than emitting SC1090. The JSVAL island absorbs the namespace + // object; callers that destructure it receive `undefined` for every export, + // which is the correct runtime behavior when the module delegates to the JS + // engine or is not reachable from the native binary. + if (dep !== null && L.dynamic) { + const promiseCtor: IrExpr = { kind: "jsOp", op: "globalGet", name: "Promise", args: [], type: JSVAL, loc }; + const emptyObj: IrExpr = { kind: "jsOp", op: "objLit", args: [], type: JSVAL, loc }; + const resolved: IrExpr = { kind: "jsOp", op: "callMethod", name: "resolve", args: [promiseCtor, emptyObj], type: JSVAL, loc }; + return { kind: "jsBridgePromise", value: resolved, type: { kind: "promise", inner: JSVAL }, loc }; + } L.unsupported( "SC1090", call, diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index a0006bf3a..0a2e9d814 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -81,6 +81,9 @@ export interface FileParts { if (!dep || dep.isDeclarationFile) return null; if (dep.fileName.endsWith(".json") || dep.fileName.endsWith(".cts")) return null; if (isCjsJsFile(dep)) return null; + // JSX files are not supported by scriptc's native compiler. + // Dynamic imports of .tsx/.jsx files resolve to an empty namespace stub at runtime. + if (dep.fileName.endsWith(".tsx") || dep.fileName.endsWith(".jsx")) return null; return dep; }