mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-14 04:15:51 +08:00
Island user program modules end to end
A file the island decision put in the set is no longer compiled natively: it is keyed by its absolute path, its declarations type the program as foreign, its diagnostics do not fail the program the build does not compile, and every import or require of it lowers to an island load. `--node-island=<path>` therefore takes effect for a user module, not only for a package. The recovery stays one round: when the lowering refuses a user JavaScript module, the reload adds every such module at once and each one is reported once as an SC8001 note naming the module and its reason. The build reports the islands it ran (packages and modules) with its result, and an executable cache hit reports them again, so a runner can tell an island result from a native one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
794ede51db
commit
678ddf8a4e
@@ -165,6 +165,10 @@ export type CompileFailure = {
|
||||
export interface CompileWarnings {
|
||||
warnings?: ScrDiagnostic[];
|
||||
sourceTexts?: Map<string, string>;
|
||||
/** The Node islands the build ran: the npm packages and the program
|
||||
* modules (absolute file paths). Reported again on an executable cache
|
||||
* hit, so a runner can tell an island result from a native one. */
|
||||
islands?: { packages: string[]; modules: string[] };
|
||||
}
|
||||
|
||||
export type CompileSourceResult =
|
||||
|
||||
@@ -453,6 +453,22 @@ export function nodeIslandNoteDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8001 — a program module runs in a Node island (a note, once per island
|
||||
* module in the build output; DESIGN.md note 1 §3.2). */
|
||||
export function nodeIslandModuleNoteDiag(
|
||||
file: string,
|
||||
klass: string,
|
||||
reason: string,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC8001",
|
||||
message: `module '${file}' runs in a Node island: ${klass}: ${reason}`,
|
||||
loc,
|
||||
hint: "sloppy-mode code Node runs; --node-islands=off compiles this module natively instead",
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8010 — a value crosses an island boundary in a way NI-4 cannot keep
|
||||
* exact yet (callbacks, native objects, remote promises). */
|
||||
export function nodeIslandBoundaryDiag(what: string, loc: SrcLoc): ScrDiagnostic {
|
||||
|
||||
@@ -147,6 +147,8 @@ interface EarlyExecutableCacheStamp {
|
||||
native: EarlyExecutableNativeFeatures;
|
||||
/** The build's performance warnings (SC61xx), which a cache hit repeats. */
|
||||
performance?: ScrDiagnostic[];
|
||||
/** The Node islands the build ran, which a cache hit reports again. */
|
||||
islands?: { packages: string[]; modules: string[] };
|
||||
/** Programs that call into Node.js: the libnode pack identity the build
|
||||
* recorded (EarlyExecutableCacheOptions.libnodeIdentity). */
|
||||
libnode?: string;
|
||||
@@ -259,6 +261,8 @@ export interface EarlyExecutableCacheHit {
|
||||
frontend: FrontendInputSnapshot;
|
||||
/** The performance warnings the build reported (SC61xx), repeated on a hit. */
|
||||
performance?: ScrDiagnostic[];
|
||||
/** The Node islands the build ran, repeated on a hit. */
|
||||
islands?: { packages: string[]; modules: string[] };
|
||||
/** How a --profile-use build's profile matched, reported again on a hit. */
|
||||
profileMatch?: NativeProfileMatch;
|
||||
}
|
||||
@@ -267,6 +271,16 @@ export interface EarlyExecutableCachePublish extends EarlyExecutableCacheHit {
|
||||
nativeDependencies?: NativeArtifactDependency[];
|
||||
}
|
||||
|
||||
function isIslands(value: unknown): value is { packages: string[]; modules: string[] } {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const islands = value as Record<string, unknown>;
|
||||
return (
|
||||
Array.isArray(islands.packages) &&
|
||||
Array.isArray(islands.modules) &&
|
||||
[...islands.packages, ...islands.modules].every((item) => typeof item === "string")
|
||||
);
|
||||
}
|
||||
|
||||
function isProfileMatch(value: unknown): value is NativeProfileMatch {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const match = value as Record<string, unknown>;
|
||||
@@ -745,6 +759,7 @@ async function readExecutableCache(
|
||||
...(Array.isArray(stamp.performance) && stamp.performance.length > 0
|
||||
? { performance: stamp.performance }
|
||||
: {}),
|
||||
...(isIslands(stamp.islands) ? { islands: stamp.islands } : {}),
|
||||
...(isProfileMatch(stamp.profileMatch) ? { profileMatch: stamp.profileMatch } : {}),
|
||||
...(ir === null ? {} : { irPath: paths.irPath }),
|
||||
...(llvmSourcePath === undefined ? {} : { llvmSourcePath }),
|
||||
@@ -936,6 +951,7 @@ export async function publishEarlyExecutableCache(
|
||||
...(result.performance === undefined || result.performance.length === 0
|
||||
? {}
|
||||
: { performance: result.performance }),
|
||||
...(result.islands === undefined ? {} : { islands: result.islands }),
|
||||
...(result.native.nodeIsland === true ? { libnode: options.libnodeIdentity?.() ?? "" } : {}),
|
||||
...(result.profileMatch === undefined ? {} : { profileMatch: result.profileMatch }),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { realpathSync } from "node:fs";
|
||||
import type { CompileFailure, CompileRequestOptions } from "../compile-types.js";
|
||||
import type { FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { frontendDisposed } from "../frontend/dispose-hook.js";
|
||||
import {
|
||||
checkerPanicDiag,
|
||||
nodeIslandModuleNoteDiag,
|
||||
nodeIslandNoteDiag,
|
||||
nodeIslandRetryFailedDiag,
|
||||
npmStaticRefusalDiag,
|
||||
@@ -24,15 +26,20 @@ import type { FrontendFactory } from "../frontend/pipeline.js";
|
||||
import type { UnitStore } from "../frontend/lowering/unit-reuse.js";
|
||||
import type { CompilationTiming } from "../timing.js";
|
||||
import { readProjectConfig } from "../project-config.js";
|
||||
import { nodeIslandPolicy } from "../node-islands/policy.js";
|
||||
import {
|
||||
isMarkedUserModule,
|
||||
nodeIslandPolicy,
|
||||
type NodeIslandPolicy,
|
||||
} from "../node-islands/policy.js";
|
||||
import { buildNodeIslandReport } from "../node-islands/report.js";
|
||||
import type { IslandPackage, NodeIslandSet } from "../node-islands/boundary.js";
|
||||
import type { IslandModule, IslandPackage, NodeIslandSet } from "../node-islands/boundary.js";
|
||||
import { classifyDiagnostic, isIslandReasonCode } from "../node-islands/classify.js";
|
||||
import { packageDirOf, packageVersionOf } from "../node-islands/origin.js";
|
||||
import { describe } from "../node-islands/summary.js";
|
||||
import { resolveBareModule } from "../frontend/resolve.js";
|
||||
import { isEsmFile } from "../frontend/lowering/lower-node-island.js";
|
||||
import type { PackageEdge } from "../node-islands/scan.js";
|
||||
import type { NodeIslandScan, PackageEdge } from "../node-islands/scan.js";
|
||||
import { isJsSourceFileName } from "../frontend/tsc-codes.js";
|
||||
|
||||
export interface PreparedExecutableModule {
|
||||
ok: true;
|
||||
@@ -42,6 +49,10 @@ export interface PreparedExecutableModule {
|
||||
/** Divergence warnings (SC6xxx): the build succeeds; these sites can
|
||||
* behave differently from Node. */
|
||||
warnings: ScrDiagnostic[];
|
||||
/** The Node islands the build ran (the SC8001 notes name them with their
|
||||
* reasons): packages by name, modules by file. A cached build repeats
|
||||
* them. */
|
||||
islands?: { packages: string[]; modules: string[] };
|
||||
}
|
||||
|
||||
/** Release the parser/checker before native object generation begins.
|
||||
@@ -57,6 +68,7 @@ export function prepareExecutableModule(
|
||||
lowerIslands: ReadonlyMap<string, IslandPackage> = new Map(),
|
||||
retryFences = true,
|
||||
unitStore: UnitStore | null = null,
|
||||
lowerModules: ReadonlyMap<string, IslandModule> = new Map(),
|
||||
): PreparedExecutableModule | CompileFailure {
|
||||
// The island decision reads the detector's scan unless islands are off.
|
||||
const islandScan =
|
||||
@@ -66,11 +78,14 @@ export function prepareExecutableModule(
|
||||
// Node islands (scope-B NI-4): packages refused natively run in Node,
|
||||
// unless the policy says otherwise. The frontend reloads without them,
|
||||
// so their declarations type the program.
|
||||
const islandDecision = decideNodeIslands(entryPath, opts, fe, lowerIslands);
|
||||
const islandDecision = decideNodeIslands(entryPath, opts, fe, lowerIslands, lowerModules);
|
||||
if (islandDecision !== null) {
|
||||
fe.dispose();
|
||||
fe = createFrontend(entryPath, opts.npmStatic, undefined, undefined, {
|
||||
islandPackages: new Set(islandDecision.packages.keys()),
|
||||
...(islandDecision.modules.size > 0
|
||||
? { islandModules: new Set(islandDecision.modules.keys()) }
|
||||
: {}),
|
||||
islandScan,
|
||||
});
|
||||
timing("frontend-island-reload");
|
||||
@@ -121,6 +136,7 @@ export function prepareExecutableModule(
|
||||
grown,
|
||||
retryFences,
|
||||
unitStore,
|
||||
lowerModules,
|
||||
);
|
||||
}
|
||||
return fail(fe.preflight);
|
||||
@@ -159,7 +175,11 @@ export function prepareExecutableModule(
|
||||
lowerIslands,
|
||||
fe.nodeIslandScan?.({ userJs: false }).edges,
|
||||
);
|
||||
if (grown !== null) {
|
||||
// Sloppy user modules the lowering refused join the same retry
|
||||
// (DESIGN.md note 1 §3.2): one reload with every island at once,
|
||||
// never one round per module.
|
||||
const grownModules = loweringModules(entryPath, opts, lowered.diagnostics, lowerModules);
|
||||
if (grown !== null || grownModules !== null) {
|
||||
fe.dispose();
|
||||
frontendDisposed();
|
||||
disposed = true;
|
||||
@@ -170,9 +190,10 @@ export function prepareExecutableModule(
|
||||
buildPlatform,
|
||||
createFrontend,
|
||||
timing,
|
||||
grown,
|
||||
grown ?? lowerIslands,
|
||||
retryFences,
|
||||
unitStore,
|
||||
grownModules ?? lowerModules,
|
||||
);
|
||||
}
|
||||
// A failed program whose compiled packages need the runtime or a
|
||||
@@ -198,6 +219,8 @@ export function prepareExecutableModule(
|
||||
timing,
|
||||
marked,
|
||||
false,
|
||||
undefined,
|
||||
lowerModules,
|
||||
);
|
||||
}
|
||||
return fail(lowered.diagnostics);
|
||||
@@ -258,6 +281,14 @@ export function prepareExecutableModule(
|
||||
...(lowered.divergences ?? []),
|
||||
...(lowered.performance ?? []),
|
||||
],
|
||||
...(islandDecision === null
|
||||
? {}
|
||||
: {
|
||||
islands: {
|
||||
packages: [...islandDecision.packages.keys()].sort(),
|
||||
modules: [...islandDecision.modules.keys()].sort(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
// Packages compiled with runtime fences that need the JavaScript
|
||||
// runtime or a mutable object model (fenceIslands): build again with
|
||||
@@ -284,6 +315,7 @@ export function prepareExecutableModule(
|
||||
islands,
|
||||
false,
|
||||
unitStore,
|
||||
lowerModules,
|
||||
);
|
||||
if (islanded.ok) return islanded;
|
||||
firstError ??= islanded.diagnostics[0] ?? null;
|
||||
@@ -298,16 +330,63 @@ export function prepareExecutableModule(
|
||||
return prepared;
|
||||
}
|
||||
|
||||
/** The packages a build runs in Node islands, or null for none: under
|
||||
* `--node-islands=auto` (the default) every npm package the native
|
||||
* pipeline refused, plus packages marked `node`; under `explicit` only the
|
||||
* marked ones; under `off`, none (refusals stay SC2013). Each island is
|
||||
* reported once as an SC8001 note. */
|
||||
/** Whether two spellings name the same file (the checker resolves through
|
||||
* symlinks; a CLI argument may not). */
|
||||
function sameFile(a: string, b: string): boolean {
|
||||
if (samePath(a, b)) return true;
|
||||
try {
|
||||
const real = realpathSync(a);
|
||||
const other = realpathSync(b);
|
||||
return samePath(real, other);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function samePath(a: string, b: string): boolean {
|
||||
return a.replaceAll("\\", "/") === b.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
/** The program modules that run in Node islands, by file: modules marked
|
||||
* with `--node-island=<path>` or `scriptc.nodeIslands.modules`. The entry
|
||||
* stays native: a program whose entry is an island runs the whole file in
|
||||
* Node, which is a different feature. */
|
||||
function moduleIslands(
|
||||
entryPath: string,
|
||||
policy: NodeIslandPolicy,
|
||||
scan: NodeIslandScan,
|
||||
): Map<string, IslandModule> {
|
||||
const modules = new Map<string, IslandModule>();
|
||||
for (const m of scan.modules) {
|
||||
if (m.origin !== "user") continue;
|
||||
if (!isJsSourceFileName(m.file)) continue;
|
||||
if (sameFile(m.file, entryPath)) continue;
|
||||
const marked = isMarkedUserModule(policy, m.file);
|
||||
if (marked === null) continue;
|
||||
modules.set(m.file, {
|
||||
file: m.file,
|
||||
class: "marked",
|
||||
reason:
|
||||
marked === "cli"
|
||||
? "marked with --node-island"
|
||||
: 'marked "node" in scriptc.nodeIslands.modules',
|
||||
decision: "marked",
|
||||
});
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
/** The packages and modules a build runs in Node islands, or null for none:
|
||||
* under `--node-islands=auto` (the default) every npm package the native
|
||||
* pipeline refused, plus the packages and modules marked `node`; under
|
||||
* `explicit` only the marked ones; under `off`, none (refusals stay
|
||||
* SC2013). Each island is reported once as an SC8001 note. */
|
||||
function decideNodeIslands(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
fe: ReturnType<FrontendFactory>,
|
||||
lowerIslands: ReadonlyMap<string, IslandPackage>,
|
||||
lowerModules: ReadonlyMap<string, IslandModule>,
|
||||
): (NodeIslandSet & { notes: ScrDiagnostic[] }) | null {
|
||||
const policy = nodeIslandPolicy(readProjectConfig(entryPath), opts.nodeIslands);
|
||||
if (policy.mode === "off" || fe.nodeIslandScan === undefined) return null;
|
||||
@@ -321,10 +400,15 @@ function decideNodeIslands(
|
||||
: [],
|
||||
);
|
||||
for (const name of lowerIslands.keys()) refused.add(name);
|
||||
if (refused.size === 0 && policy.forcedNode.size === 0) return null;
|
||||
const scan = fe.nodeIslandScan({ userJs: false });
|
||||
const modules = moduleIslands(entryPath, policy, scan);
|
||||
// Modules the lowering refused keep their first reason (the reload no
|
||||
// longer compiles them).
|
||||
for (const [file, island] of lowerModules) if (!modules.has(file)) modules.set(file, island);
|
||||
if (refused.size === 0 && policy.forcedNode.size === 0 && modules.size === 0) return null;
|
||||
const report = buildNodeIslandReport({
|
||||
entry: entryPath,
|
||||
scan: fe.nodeIslandScan({ userJs: false }),
|
||||
scan,
|
||||
npm: fe.npmStatic,
|
||||
diagnostics: [],
|
||||
fences: [],
|
||||
@@ -397,7 +481,17 @@ function decideNodeIslands(
|
||||
),
|
||||
);
|
||||
}
|
||||
return packages.size === 0 ? null : { packages, notes };
|
||||
for (const island of [...modules.values()].sort((a, b) =>
|
||||
a.file < b.file ? -1 : a.file > b.file ? 1 : 0,
|
||||
))
|
||||
notes.push(
|
||||
nodeIslandModuleNoteDiag(island.file, island.class, island.reason, {
|
||||
file: island.file,
|
||||
start: 0,
|
||||
end: 0,
|
||||
}),
|
||||
);
|
||||
return packages.size === 0 && modules.size === 0 ? null : { packages, modules, notes };
|
||||
}
|
||||
|
||||
/** The island set grown by the npm packages whose own modules refused to
|
||||
@@ -478,6 +572,41 @@ function loweringIslands(
|
||||
return grown.size === known.size ? null : grown;
|
||||
}
|
||||
|
||||
/** The island modules grown by the lowering's refusals in user JavaScript
|
||||
* (`--node-islands=auto` only), or null when there are none: a sloppy
|
||||
* module whose semantics the native pipeline refuses (an implicit global,
|
||||
* a mapped `arguments`, a `with` the pre-scan could not decide) runs in
|
||||
* Node. One retry adds every one of them at once. */
|
||||
function loweringModules(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
diagnostics: readonly ScrDiagnostic[],
|
||||
known: ReadonlyMap<string, IslandModule>,
|
||||
): Map<string, IslandModule> | null {
|
||||
const policy = nodeIslandPolicy(readProjectConfig(entryPath), opts.nodeIslands);
|
||||
if (policy.mode !== "auto" || known.size > 16) return null;
|
||||
const grown = new Map(known);
|
||||
for (const d of diagnostics) {
|
||||
if (!isIslandReasonCode(d.code)) continue;
|
||||
const file = d.loc.file;
|
||||
// Packages run in Node through their own decision (loweringIslands).
|
||||
if (file.includes("/node_modules/") || packageDirOf(file) !== null) continue;
|
||||
if (!isJsSourceFileName(file) || sameFile(file, entryPath)) continue;
|
||||
if (grown.has(file)) continue;
|
||||
const reason = classifyDiagnostic(d, "refusal");
|
||||
// A gap is a construct scriptc does not implement, not a sloppy
|
||||
// semantic: running it in Node would hide a compiler limitation.
|
||||
if (reason.class === "gap") continue;
|
||||
grown.set(file, {
|
||||
file,
|
||||
class: reason.class,
|
||||
reason: describe(reason),
|
||||
decision: "auto",
|
||||
});
|
||||
}
|
||||
return grown.size === known.size ? null : grown;
|
||||
}
|
||||
|
||||
/** Preflight refusals at import statements of natively compiled npm
|
||||
* packages (`--node-islands=auto` only): those packages become islands. */
|
||||
function preflightIslands(
|
||||
|
||||
@@ -665,6 +665,10 @@ 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.
|
||||
// A relative import of a program module the pre-scan put in a Node
|
||||
// island (DESIGN.md note 1 §3.2): the island loads at this module's
|
||||
// position and its bindings are island values.
|
||||
if (lowerer.nodeIslands !== null && collectIslandImport(lowerer, fp.sf, stmt, spec)) continue;
|
||||
const projectDep = isRelativeSpecifier(spec)
|
||||
? null
|
||||
: resolveImport(lowerer.program, fp.sf, spec);
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
* checked against the declared result type, like `JSON.parse(s) as T`; a
|
||||
* thrown Error crosses as its nearest intrinsic class with its message and
|
||||
* string `code`. */
|
||||
import { readFileSync, realpathSync } from "node:fs";
|
||||
import { readFileSync, realpathSync, statSync } from "node:fs";
|
||||
import { resolveFrom } from "../../node-islands/resolve.js";
|
||||
import { dirname, isAbsolute, join } from "node:path";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { isRelativeSpecifier } from "../workspace-registry.js";
|
||||
import type { NodeApiMember } from "../node-api-import.js";
|
||||
import { PoisonError, newFnCtx, type Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
@@ -49,7 +50,7 @@ import {
|
||||
isForeignType,
|
||||
isIslandSubclassDecl,
|
||||
markIslandSubclass,
|
||||
islandPackageOfFile,
|
||||
islandOwnerOfFile,
|
||||
type NodeIslandSet,
|
||||
} from "../../node-islands/boundary.js";
|
||||
import { stripTypeWrappers } from "./typed-arguments.js";
|
||||
@@ -288,21 +289,54 @@ export function isEsmFile(file: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** The file a relative (or absolute) specifier names from `fromFile`, as
|
||||
* Node's CommonJS/ESM resolution would find it, or null. Extension fallback
|
||||
* and directory indexes only: an island module is a file the program
|
||||
* already loaded, and `exports` maps stay Node's job. */
|
||||
function relativeIslandFile(fromFile: string, spec: string): string | null {
|
||||
const base = isAbsolute(spec) ? spec : resolve(dirname(fromFile), spec);
|
||||
const candidates = [base];
|
||||
if (!/\.[cm]?js$/.test(base)) {
|
||||
for (const ext of [".js", ".cjs", ".mjs"]) candidates.push(base + ext);
|
||||
}
|
||||
for (const ext of [".js", ".cjs", ".mjs"]) candidates.push(join(base, `index${ext}`));
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
if (statSync(candidate).isFile()) return candidate;
|
||||
} catch {
|
||||
/* the next candidate */
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The island module file `spec` names from `fromFile`, or null when the
|
||||
* specifier does not resolve into an island package. */
|
||||
* specifier does not resolve into an island package or module. */
|
||||
export function islandModuleFile(
|
||||
islands: NodeIslandSet,
|
||||
fromFile: string,
|
||||
spec: string,
|
||||
kind: "import" | "require",
|
||||
): string | null {
|
||||
const js = resolveBareModule(fromFile, spec, "js-only", kind);
|
||||
if (js === null) return null;
|
||||
if (islandPackageOfFile(islands, js.typesFile) === null) return null;
|
||||
let file: string;
|
||||
if (isRelativeSpecifier(spec) || isAbsolute(spec)) {
|
||||
// A program module the pre-scan put in a Node island (DESIGN.md note 1
|
||||
// §3.2): the file itself is the island, resolved as Node would.
|
||||
if (islands.modules.size === 0) return null;
|
||||
const resolved = relativeIslandFile(fromFile, spec);
|
||||
if (resolved === null) return null;
|
||||
if (islandOwnerOfFile(islands, resolved) === null) return null;
|
||||
file = resolved;
|
||||
} else {
|
||||
const js = resolveBareModule(fromFile, spec, "js-only", kind);
|
||||
if (js === null) return null;
|
||||
if (islandOwnerOfFile(islands, js.typesFile) === null) return null;
|
||||
file = js.typesFile;
|
||||
}
|
||||
try {
|
||||
return realpathSync(js.typesFile);
|
||||
return realpathSync(file);
|
||||
} catch {
|
||||
return js.typesFile;
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -415,6 +415,10 @@ export function sharedModulePlans(lowerer: Lowerer): SharedModulePlan[] {
|
||||
}
|
||||
for (const island of lowerer.nodeIslands?.packages.values() ?? [])
|
||||
packageJsFiles(island.dir, queue);
|
||||
// A program module the build runs in a Node island is loaded by Node, so
|
||||
// the program modules IT imports are shared with it like an island
|
||||
// package's (DESIGN.md note 1 §3.2).
|
||||
for (const module of lowerer.nodeIslands?.modules.values() ?? []) queue.push(real(module.file));
|
||||
if (queue.length === 0 && candidates.size === 0) return plans;
|
||||
const seen = new Set<string>();
|
||||
// Classes island code extends, by the compiled module that exports them.
|
||||
|
||||
@@ -146,6 +146,10 @@ export interface FrontendOptions {
|
||||
/** Packages that run in Node islands (scope-B NI-4): never compiled,
|
||||
* typed by their declarations. */
|
||||
islandPackages?: ReadonlySet<string>;
|
||||
/** Program modules that run in Node islands (DESIGN.md note 1 §3.2):
|
||||
* their bodies are never compiled; their imports lower to island loads.
|
||||
* Absolute file paths. */
|
||||
islandModules?: ReadonlySet<string>;
|
||||
/** The Node island report (coverage): the project's scriptc settings and
|
||||
* the detector's scan of the program and its npm packages. Builds do not
|
||||
* read either. */
|
||||
@@ -565,6 +569,11 @@ export function runFrontend(
|
||||
}
|
||||
}
|
||||
|
||||
/** A file path as the frontend and the island decision spell it. */
|
||||
function normalizeFile(file: string): string {
|
||||
return file.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
/** Rounds of dropping single refused overlay annotations before the
|
||||
* fallback drops whole files' overlays. */
|
||||
const OVERLAY_ANNOTATION_ROUNDS = 4;
|
||||
@@ -586,6 +595,18 @@ function loadFrontend(
|
||||
beginShapeOverlayBuild();
|
||||
const statuses: NpmStaticStatus[] = [];
|
||||
const npmSites = new Map<string, SrcLoc>();
|
||||
// Program modules that run in Node islands (DESIGN.md note 1 §3.2): their
|
||||
// bodies are not lowered, so their sloppy semantics stay Node's, and a
|
||||
// diagnostic anchored in one does not fail the program the build does not
|
||||
// compile.
|
||||
const islandModules =
|
||||
options.islandModules === undefined || options.islandModules.size === 0
|
||||
? null
|
||||
: new Set([...options.islandModules].map(normalizeFile));
|
||||
const inIslandModule = (loc: SrcLoc | undefined): boolean =>
|
||||
islandModules !== null && loc !== undefined && islandModules.has(normalizeFile(loc.file));
|
||||
const withoutIslandModules = (diags: ScrDiagnostic[]): ScrDiagnostic[] =>
|
||||
islandModules === null ? diags : diags.filter((d) => !inIslandModule(d.loc));
|
||||
// Node island detection (the report, or a build that may run islands):
|
||||
// how each package was refused, and the modules of packages removed from
|
||||
// the program.
|
||||
@@ -848,8 +869,9 @@ function loadFrontend(
|
||||
const patchDiagnostics = typePatchDiagnostics()?.diagnostics ?? [];
|
||||
const buildWarnings = [...(report ? [] : configDiagnostics), ...patchDiagnostics];
|
||||
return {
|
||||
preflight:
|
||||
preflight: withoutIslandModules(
|
||||
report && configDiagnostics.length > 0 ? [...configDiagnostics, ...preflight] : preflight,
|
||||
),
|
||||
...(buildWarnings.length > 0 ? { warnings: buildWarnings } : {}),
|
||||
entryText: () => finalLoad.entry.text,
|
||||
entryExports: () => entryFunctionExports(finalLoad.entry),
|
||||
@@ -896,9 +918,22 @@ function loadFrontend(
|
||||
return texts;
|
||||
},
|
||||
lower: (opts, valid) => {
|
||||
// The module order without the island modules, computed once per load:
|
||||
// a reload keeps the array the lowering appends its dynamic import
|
||||
// modules to.
|
||||
const islandFiltered = new WeakMap<readonly SourceFile[], SourceFile[]>();
|
||||
const lowerOrder = (l: typeof finalLoad): SourceFile[] => {
|
||||
if (islandModules === null) return l.moduleOrder;
|
||||
let kept = islandFiltered.get(l.moduleOrder);
|
||||
if (kept === undefined) {
|
||||
kept = l.moduleOrder.filter((sf) => !islandModules.has(normalizeFile(sf.fileName)));
|
||||
islandFiltered.set(l.moduleOrder, kept);
|
||||
}
|
||||
return kept;
|
||||
};
|
||||
const lowerLoad = (l: typeof finalLoad): LowerResult => {
|
||||
if (l.shapeOverlayState !== undefined) restoreShapeOverlayState(l.shapeOverlayState);
|
||||
return lowerToIr(l.program, l.entry, l.moduleOrder, {
|
||||
return lowerToIr(l.program, l.entry, lowerOrder(l), {
|
||||
...opts,
|
||||
frontendServices: l.services,
|
||||
startupCrash: l.startupCrash ?? null,
|
||||
@@ -964,7 +999,7 @@ function loadFrontend(
|
||||
const t1 = performance.now();
|
||||
// The preflight fills the module order; the tsc world reads the
|
||||
// files as written, so it answers as the first load did.
|
||||
const reloadPreflight = checkPreflight(finalLoad);
|
||||
const reloadPreflight = withoutIslandModules(checkPreflight(finalLoad));
|
||||
if (reloadPreflight.length > 0)
|
||||
return { ...lowered, module: null, diagnostics: reloadPreflight };
|
||||
const t2 = performance.now();
|
||||
@@ -1066,7 +1101,7 @@ function loadFrontend(
|
||||
setShapeOverlayDropped(true);
|
||||
finalLoad.dispose();
|
||||
finalLoad = loadProgram(entryPath, { npmStatic: finalNpmStatic, externalTypes });
|
||||
const reloadPreflight = checkPreflight(finalLoad);
|
||||
const reloadPreflight = withoutIslandModules(checkPreflight(finalLoad));
|
||||
if (reloadPreflight.length > 0)
|
||||
return { ...lowered, module: null, diagnostics: reloadPreflight };
|
||||
lowered = lowerOnce();
|
||||
|
||||
@@ -539,10 +539,11 @@ export async function compile(
|
||||
}
|
||||
|
||||
/** Divergence warnings collected while a compile lowers (none when a cache
|
||||
* hit skips lowering). */
|
||||
* hit skips lowering), and the Node islands the build ran. */
|
||||
interface CompileWarningSink {
|
||||
warnings: ScrDiagnostic[];
|
||||
sourceTexts: Map<string, string> | null;
|
||||
islands?: { packages: string[]; modules: string[] };
|
||||
}
|
||||
|
||||
/** The package.json texts type patch warnings point into, re-read for a
|
||||
@@ -560,6 +561,15 @@ function cachedWarningSources(warnings: readonly ScrDiagnostic[]): Map<string, s
|
||||
return texts;
|
||||
}
|
||||
|
||||
/** The Node islands a build ran, stored with its cache entry so a cache hit
|
||||
* reports them again (a runner reads them to tell an island pass from a
|
||||
* native one). */
|
||||
function islandFields(sink: CompileWarningSink | undefined): {
|
||||
islands?: { packages: string[]; modules: string[] };
|
||||
} {
|
||||
return sink?.islands === undefined ? {} : { islands: sink.islands };
|
||||
}
|
||||
|
||||
/** The build's performance warnings (SC61xx), stored with its cache entry
|
||||
* so a cache hit repeats them. */
|
||||
function performanceWarnings(sink: CompileWarningSink | undefined): {
|
||||
@@ -585,8 +595,15 @@ function withCompileWarnings<T extends CompileRequestResult>(
|
||||
result: T,
|
||||
sink: CompileWarningSink,
|
||||
): T {
|
||||
if (!result.ok || sink.warnings.length === 0) return result;
|
||||
return { ...result, warnings: sink.warnings, sourceTexts: sink.sourceTexts ?? new Map() };
|
||||
if (!result.ok) return result;
|
||||
const islands = sink.islands === undefined ? {} : { islands: sink.islands };
|
||||
if (sink.warnings.length === 0) return { ...result, ...islands };
|
||||
return {
|
||||
...result,
|
||||
warnings: sink.warnings,
|
||||
sourceTexts: sink.sourceTexts ?? new Map(),
|
||||
...islands,
|
||||
};
|
||||
}
|
||||
|
||||
/** Build-time API compatibility fence: a caller's existing CompileOptions
|
||||
@@ -1026,6 +1043,9 @@ interface PreparedExecutable {
|
||||
nativeFeatures: EarlyExecutableNativeFeatures;
|
||||
programSplit: ReturnType<typeof splitLlvmProgram>;
|
||||
sourceTexts: Map<string, string>;
|
||||
/** The Node islands the build ran, reported with its result (a runner
|
||||
* reads them to tell an island result from a native one). */
|
||||
islands?: { packages: string[]; modules: string[] };
|
||||
}
|
||||
|
||||
/** Finish frontend work in a separate scope so the AST, checker caches and
|
||||
@@ -1103,6 +1123,8 @@ async function prepareExecutableInput(
|
||||
warningSink.warnings = prepared.warnings;
|
||||
warningSink.sourceTexts = sourceTexts;
|
||||
}
|
||||
if (warningSink !== undefined && prepared.islands !== undefined)
|
||||
warningSink.islands = prepared.islands;
|
||||
|
||||
const stem = basename(entryPath).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, "");
|
||||
const defaultSourcePaths = {
|
||||
@@ -1262,6 +1284,7 @@ async function prepareExecutableInput(
|
||||
nativeFeatures,
|
||||
programSplit,
|
||||
sourceTexts,
|
||||
...(prepared.islands === undefined ? {} : { islands: prepared.islands }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1447,6 +1470,7 @@ type FrontendProcessResult =
|
||||
nativeFeatures: EarlyExecutableNativeFeatures;
|
||||
sourceTexts: [string, string][];
|
||||
warnings: ScrDiagnostic[];
|
||||
islands?: { packages: string[]; modules: string[] };
|
||||
frontend: FrontendInputSnapshot;
|
||||
}
|
||||
| {
|
||||
@@ -1507,6 +1531,8 @@ async function prepareExecutableInputInChild(
|
||||
warningSink.warnings = result.warnings;
|
||||
warningSink.sourceTexts = sourceTexts;
|
||||
}
|
||||
if (warningSink !== undefined && result.kind === "prepared" && result.islands !== undefined)
|
||||
warningSink.islands = result.islands;
|
||||
if (result.kind === "diagnostics")
|
||||
return { ok: false, diagnostics: result.diagnostics, sourceTexts };
|
||||
return {
|
||||
@@ -1518,6 +1544,7 @@ async function prepareExecutableInputInChild(
|
||||
nativeFeatures: result.nativeFeatures,
|
||||
programSplit: null,
|
||||
sourceTexts,
|
||||
...(result.islands === undefined ? {} : { islands: result.islands }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1595,6 +1622,7 @@ export async function runFrontendProcess(requestPath: string, resultPath: string
|
||||
nativeFeatures: prepared.nativeFeatures,
|
||||
sourceTexts: [...prepared.sourceTexts],
|
||||
warnings: warnings.warnings,
|
||||
...(prepared.islands === undefined ? {} : { islands: prepared.islands }),
|
||||
frontend,
|
||||
};
|
||||
} else if (!prepared.ok) {
|
||||
@@ -2007,6 +2035,8 @@ async function compileTracked(
|
||||
warningSink.warnings = earlyHit.performance;
|
||||
warningSink.sourceTexts = cachedWarningSources(earlyHit.performance);
|
||||
}
|
||||
if (warningSink !== undefined && earlyHit.islands !== undefined)
|
||||
warningSink.islands = earlyHit.islands;
|
||||
const executableCacheOptions = earlyCacheOptions;
|
||||
// Route/proof metadata is independently evictable. A full-compiler
|
||||
// fallback that still finds the validated payload repairs that lightweight
|
||||
@@ -2253,6 +2283,7 @@ async function compileTracked(
|
||||
frontend: frontendInputs.snapshot(),
|
||||
...(irSourcePath === undefined ? {} : { irPath: irSourcePath }),
|
||||
...performanceWarnings(warningSink),
|
||||
...islandFields(warningSink),
|
||||
...profileMatchField(nativeProgramObject),
|
||||
});
|
||||
publishedExecutable = true;
|
||||
@@ -2275,6 +2306,7 @@ async function compileTracked(
|
||||
frontend: frontendInputs.snapshot(),
|
||||
...(irSourcePath === undefined ? {} : { irPath: irSourcePath }),
|
||||
...performanceWarnings(warningSink),
|
||||
...islandFields(warningSink),
|
||||
...profileMatchField(nativeProgramObject),
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
|
||||
@@ -28,9 +28,45 @@ export interface IslandPackage {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** The program's island packages, by package name. */
|
||||
/** One program module the build runs in a Node island (DESIGN.md
|
||||
* note 1 §3.2): a user JavaScript file whose semantics need the JavaScript
|
||||
* engine, keyed by its absolute path. */
|
||||
export interface IslandModule {
|
||||
/** The module file (absolute, as the checker spells it). */
|
||||
file: string;
|
||||
/** The island class (runtime, object-model, gap, packaging) and the first
|
||||
* reason, for the SC8001 note. */
|
||||
class: string;
|
||||
reason: string;
|
||||
/** Marked by the user (`--node-island`, `nodeIslands.modules`) or
|
||||
* detected by the pre-scan under `--node-islands=auto`. */
|
||||
decision: "marked" | "auto";
|
||||
}
|
||||
|
||||
/** The program's island packages, by package name, and island modules, by
|
||||
* absolute file path. */
|
||||
export interface NodeIslandSet {
|
||||
packages: ReadonlyMap<string, IslandPackage>;
|
||||
modules: ReadonlyMap<string, IslandModule>;
|
||||
}
|
||||
|
||||
function normalizePath(file: string): string {
|
||||
return file.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
/** The island module at `file`, or null. */
|
||||
export function islandModuleOfFile(islands: NodeIslandSet, file: string): IslandModule | null {
|
||||
if (islands.modules.size === 0) return null;
|
||||
return islands.modules.get(file) ?? islands.modules.get(normalizePath(file)) ?? null;
|
||||
}
|
||||
|
||||
/** The island (package or module) that owns a declaration or JavaScript
|
||||
* file, or null. Modules first: a user module is never inside a package. */
|
||||
export function islandOwnerOfFile(
|
||||
islands: NodeIslandSet,
|
||||
file: string,
|
||||
): IslandPackage | IslandModule | null {
|
||||
return islandModuleOfFile(islands, file) ?? islandPackageOfFile(islands, file);
|
||||
}
|
||||
|
||||
/** The island package that owns a declaration or JavaScript file, or null.
|
||||
@@ -65,7 +101,7 @@ function declaredInIsland(islands: NodeIslandSet, symbol: ts.Symbol | null | und
|
||||
if (decls.length === 0) return false;
|
||||
if (islandSubclassDecls.size > 0 && decls.some((d) => islandSubclassDecls.has(d))) return true;
|
||||
return decls.every(
|
||||
(d) => islandPackageOfFile(islands, ts.getSourceFileOfNode(d).fileName) !== null,
|
||||
(d) => islandOwnerOfFile(islands, ts.getSourceFileOfNode(d).fileName) !== null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,7 +186,8 @@ export function crossingOf(
|
||||
let active: NodeIslandSet | null = null;
|
||||
|
||||
export function setActiveNodeIslands(islands: NodeIslandSet | null): void {
|
||||
active = islands !== null && islands.packages.size > 0 ? islands : null;
|
||||
active =
|
||||
islands !== null && (islands.packages.size > 0 || islands.modules.size > 0) ? islands : null;
|
||||
}
|
||||
|
||||
export function activeNodeIslands(): NodeIslandSet | null {
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
* become islands only when marked (`nodeIslands.modules`,
|
||||
* `--node-island=<path>`); `"native"` (`--native=<pkg>`) makes one
|
||||
* dependency strict; `--node-islands=off` makes the whole program strict.
|
||||
*
|
||||
* This version is report-only: the policy decides what WOULD run in Node,
|
||||
* and coverage reports it, but code generation does not change. */
|
||||
* The decision drives code generation: an island package or module is not
|
||||
* compiled natively but loaded by the program's Node island. */
|
||||
import { isAbsolute, join, resolve, dirname } from "node:path";
|
||||
import type { ProjectConfig } from "../project-config.js";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user