Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/compiler/src/diagnostics/diagnostic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2206,11 +2206,16 @@ export function unsupportedNativeApiDiag(
};
}

export function unsupportedPackageImportDiag(pkg: string, loc: SrcLoc): ScrDiagnostic {
export function unsupportedPackageImportDiag(
pkg: string,
loc: SrcLoc,
hint?: string,
): ScrDiagnostic {
return npmStaticRefusalDiag(
pkg,
"its implementation is not part of the compiled module graph",
loc,
hint,
);
}

Expand All @@ -2222,6 +2227,16 @@ export function unsupportedPackageValueDiag(pkg: string, loc: SrcLoc): ScrDiagno
);
}

/** The package a declaration-only `@types/<name>` package types: resolution
* can answer the types package for a program that imports `<name>`, and the
* diagnostic must name what the program imports (`@types/scope__name` types
* `@scope/name`). Any other name is returned as is. */
export function runtimePackageName(pkg: string): string {
if (!pkg.startsWith("@types/")) return pkg;
const bare = pkg.slice("@types/".length);
return bare.includes("__") ? `@${bare.replace("__", "/")}` : bare;
}

export function npmStaticRefusalDiag(
pkg: string,
detail: string,
Expand All @@ -2230,7 +2245,7 @@ export function npmStaticRefusalDiag(
): ScrDiagnostic {
return {
code: "SC2013",
message: `cannot compile npm package '${pkg}' statically: ${detail}`,
message: `cannot compile npm package '${runtimePackageName(pkg)}' statically: ${detail}`,
loc,
milestone: "later",
hint:
Expand Down
13 changes: 11 additions & 2 deletions packages/compiler/src/frontend/lowering/lower-stdlib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import {
type ScrDiagnostic,
unsupportedNativeApiDiag,
runtimePackageName,
unsupportedPackageImportDiag,
unsupportedPackageValueDiag,
} from "../../diagnostics/diagnostic.js";
Expand Down Expand Up @@ -2356,7 +2357,8 @@ function uncompiledImportPackage(lowerer: Lowerer, call: ts.CallExpression): str
if (specifier === null) return null;
const file = ts.getSourceFileOfNode(call).fileName;
if (lowerer.dynImports.get(dynImportKey(file, specifier, !literal)) !== undefined) return null;
return resolveNpmImport(file, specifier)?.packageName ?? null;
const npm = resolveNpmImport(file, specifier);
return npm === null ? null : runtimePackageName(npm.packageName);
}

/** The package a refusal names as outside the compiled module graph
Expand Down Expand Up @@ -2425,7 +2427,14 @@ function lowerResolvedImport(
// call (lower-node-island.ts, lowerIslandLoadCall).
const npm = resolveNpmImport(ts.getSourceFileOfNode(call).fileName, specifier);
if (npm !== null) {
lowerer.pushDiag(unsupportedPackageImportDiag(npm.packageName, locOf(call)));
lowerer.pushDiag(
unsupportedPackageImportDiag(
runtimePackageName(npm.packageName),
locOf(call),
"no static import names this package, so it is not compiled: add a static import of it " +
"(`import x from 'pkg'`) so it joins the module graph, or allow a Node island for it",
),
);
throw new PoisonError();
}
lowerer.refuseUnsupportedApi("'import()'", call);
Expand Down
44 changes: 43 additions & 1 deletion tests/harness/lazy-package-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* uncompiled package reports, and that refusal names the package. Under the
* automatic policy the package then runs in a Node island (corpus 9537 runs
* that against Node); with islands off the refusal is the build's error. */
import { cpSync, mkdtempSync, rmSync } from "node:fs";
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, expect, test } from "vitest";
Expand Down Expand Up @@ -33,3 +33,45 @@ test("with islands off, the build error names the lazily imported package", asyn
);
expect(result.diagnostics.map((d) => d.code)).not.toContain("SC2012");
}, 120_000);

test("the refusal names the imported package, not its @types package, and hints at a static import", async () => {
const copy = mkdtempSync(join(tmpdir(), "scriptc-lazy-types-"));
copies.push(copy);
const write = (file: string, text: string): void => {
mkdirSync(join(copy, file, ".."), { recursive: true });
writeFileSync(join(copy, file), text);
};
write("package.json", '{"name":"app","private":true,"type":"module"}\n');
write(
"node_modules/tiny-ms/package.json",
'{"name":"tiny-ms","version":"1.0.0","main":"index.js"}\n',
);
write("node_modules/tiny-ms/index.js", "module.exports = (s) => s.length;\n");
write(
"node_modules/@types/tiny-ms/package.json",
'{"name":"@types/tiny-ms","version":"1.0.0","types":"index.d.ts"}\n',
);
write(
"node_modules/@types/tiny-ms/index.d.ts",
"declare function ms(s: string): number;\nexport = ms;\n",
);
write(
"main.ts",
'async function main() {\n const m = await import("tiny-ms");\n console.log(m.default("abc"));\n}\nmain();\n',
);
const result = await compile(join(copy, "main.ts"), {
outPath: join(copy, "main"),
outDir: copy,
nodeIslands: { mode: "off" },
});
expect(result.ok).toBe(false);
if (result.ok) return;
const refusals = result.diagnostics.filter((d) => d.code === "SC2013");
expect(refusals.length).toBeGreaterThan(0);
for (const refusal of refusals) {
expect(refusal.message).toBe(
"cannot compile npm package 'tiny-ms' statically: its implementation is not part of the compiled module graph",
);
}
expect(refusals[0]?.hint).toContain("add a static import");
}, 120_000);
Loading