Merge base branch

This commit is contained in:
Malte Ubl
2026-10-26 05:34:56 -07:00
14 changed files with 146 additions and 1 deletions
@@ -636,7 +636,13 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void {
// graph: resolveImport follows its node_modules link to the loaded
// TypeScript entry. Its internal `.js` spellings have already been
// resolved to `.ts` by that graph.
if (!isRelativeSpecifier(spec) && resolveImport(lowerer.program, fp.sf, spec) !== null) {
const projectDep = isRelativeSpecifier(spec)
? null
: resolveImport(lowerer.program, fp.sf, spec);
if (projectDep !== null) {
// A Node island package that tsconfig `paths` maps to its installed
// directory resolves to its declarations: it loads in Node.
if (projectDep.isDeclarationFile) collectIslandImport(lowerer, fp.sf, stmt, spec);
continue;
}
const npm = resolveNpmImport(fp.sf.fileName, spec);
+20
View File
@@ -5754,6 +5754,16 @@ function preflight7(load: LoadResult): {
);
continue;
}
// A re-export from a Node island package that tsconfig `paths` maps
// to its installed directory: its declarations are no program
// module (the lowering reports the island boundary).
if (
projectReexport !== null &&
projectReexport.isDeclarationFile &&
!isRelativeSpecifier(fromSpec) &&
isFrontendIsland(packageNameOfSpecifier(fromSpec))
)
continue;
const reDep = projectReexport;
// `export * as ns from "./m"` re-exports the module NAMESPACE
// object under a name: importers' `x.ns.member` reads resolve
@@ -5876,6 +5886,16 @@ function preflight7(load: LoadResult): {
// Node's startup crash with Node's exact message; what keeps a compile
// fence is unsupported builtin or types-only surface.
if (isBare && npmStaticDep === null) {
// A Node island package that tsconfig `paths` maps to its
// installed directory resolves to its declarations here: it loads
// in Node, typed by them, like an island package resolved through
// node_modules (scope-B NI-4).
if (
projDep !== null &&
projDep.isDeclarationFile &&
isFrontendIsland(packageNameOfSpecifier(spec))
)
continue;
if (projDep !== null && projDep.isDeclarationFile) {
diags.push(
unsupportedDiag(
@@ -0,0 +1,18 @@
{
"order": [
"<repo>/tests/corpus/6839-node-island-path-mapped/main.ts"
],
"diags": [
{
"code": "SC1010",
"message": "the 'pathkit' import (it resolves only to type declarations — there is no runtime implementation to compile) is not supported yet",
"loc": {
"file": "<repo>/tests/corpus/6839-node-island-path-mapped/main.ts",
"start": 234,
"end": 294
},
"milestone": "M4",
"hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
}
]
}
@@ -0,0 +1,21 @@
// @requires-libnode
// tsconfig `paths` maps an island package to its installed directory (a
// monorepo pinning one copy): its import resolves to the package's
// declarations, which type the program, and the package loads in Node.
import { Paths, heavier, route, type Route } from "pathkit";
interface Table<T extends Route> {
title: string;
rows: T[];
}
function render<T extends Route>(table: Table<T>): string {
return `${table.title}: ${table.rows.map((r) => r.label()).join(", ")}`;
}
const routes: Route[] = [route("/a", 1), route("/b", 3), route("/c", 2)];
const table: Table<Route> = { title: "routes", rows: routes };
console.log(render(table));
const top = heavier(routes[0]!, routes[1]!);
console.log(top.rank, top.label(), top === routes[1], routes.length);
console.log(Paths.join("api", "v1"));
@@ -0,0 +1,10 @@
export interface Route {
readonly path: string;
readonly weight: number;
label(): string;
}
export type Weighted<T extends Route> = T & { readonly rank: number };
export declare function route(path: string, weight: number): Route;
export declare function heavier<T extends Route>(a: T, b: T): Weighted<T>;
import * as Paths from "./paths.mjs";
export { Paths };
@@ -0,0 +1,13 @@
// The loader picks its part with a computed require() specifier, which
// scriptc does not compile natively: the package runs in a Node island.
import { createRequire } from "node:module";
const load = createRequire(import.meta.url);
const { Route } = ["./route.cjs"].map((p) => load(p))[0];
export function route(path, weight) {
return new Route(path, weight);
}
export function heavier(a, b) {
return Object.assign(b.weight > a.weight ? b : a, { rank: 1 });
}
import * as Paths from "./paths.mjs";
export { Paths };
@@ -0,0 +1 @@
export declare function join(...parts: string[]): string;
@@ -0,0 +1,3 @@
export function join(...parts) {
return "/" + parts.join("/");
}
@@ -0,0 +1,11 @@
"use strict";
class Route {
constructor(path, weight) {
this.path = path;
this.weight = weight;
}
label() {
return this.path + "@" + this.weight;
}
}
exports.Route = Route;
@@ -0,0 +1,7 @@
{
"name": "pathkit",
"version": "2.0.0",
"type": "module",
"types": "./build/index.d.mts",
"exports": { ".": { "import": "./build/index.mjs", "default": "./build/index.mjs" } }
}
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"strict": true,
"paths": {
"pathkit": ["./node_modules/pathkit"]
}
}
}
+7
View File
@@ -0,0 +1,7 @@
// Re-exports from an island package that tsconfig `paths` maps to its
// installed directory: the type-only one is erased, the value one reports
// the island boundary, and the package's declarations never compile as a
// program module. The test builds it beside corpus 6839's pathkit package.
export type { Route } from "pathkit";
export { route } from "pathkit";
console.log("loaded");
@@ -0,0 +1 @@
{ "plain": 3300, "san": 30000 }
+19
View File
@@ -354,3 +354,22 @@ test("recursive JSON types cross into Node", async () => {
const ir = JSON.stringify(deserializeModule(readFileSync(outPath, "utf8")));
expect(ir).toContain("nodeIsland.callSync");
}, 300_000);
// Needs no libnode pack: the build fails before linking.
test("re-exporting a path-mapped island package reports the island boundary", async () => {
const dir = join(outDir, "path-mapped-reexport");
rmSync(dir, { recursive: true, force: true });
const corpus = join(repoRoot, "tests/corpus/6839-node-island-path-mapped");
cpSync(join(corpus, "node_modules"), join(dir, "node_modules"), { recursive: true });
copyFileSync(join(corpus, "tsconfig.json"), join(dir, "tsconfig.json"));
copyFileSync(
join(repoRoot, "tests/fixtures/node-island/path-mapped-reexport.ts"),
join(dir, "main.ts"),
);
const result = await compile(join(dir, "main.ts"), { outPath: join(dir, "main"), outDir: dir });
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.diagnostics.map((d) => `${d.code} ${d.loc.file.slice(dir.length + 1)}`)).toEqual([
"SC8010 main.ts",
]);
}, 300_000);