mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Islands: CommonJS island requires, builtin-class handles, JavaScript widened bindings
Node islands stack, part 10 of 11 (scriptc-perf/results/islands-prs/PLAN.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ec88372eba
commit
00e88c2dfd
@@ -36,6 +36,7 @@ import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
import { DYN, type IrExpr } from "../../ir/ir.js";
|
||||
import { nodeThrowExpr } from "../../ir/build.js";
|
||||
import { probeNodeRequireRefusal } from "../npm.js";
|
||||
import { lowerIslandRequireCall } from "./lower-node-island.js";
|
||||
|
||||
export interface InlineRequire {
|
||||
call: ts.CallExpression;
|
||||
@@ -216,6 +217,9 @@ export function lowerUncompiledRequireCall(
|
||||
);
|
||||
}
|
||||
if (canonicalBuiltinModule(spec) !== null) return null;
|
||||
// A package that runs in a Node island loads at the call (scope B).
|
||||
const island = lowerIslandRequireCall(lowerer, call);
|
||||
if (island !== null) return island;
|
||||
if (!spec.startsWith(".") && !spec.startsWith("/") && !spec.startsWith("#")) {
|
||||
const refusal = probeNodeRequireRefusal(sf.fileName, spec);
|
||||
if (refusal !== null) {
|
||||
|
||||
@@ -82,6 +82,15 @@ import { mixinFnOfCallee } from "../lower-mixins.js";
|
||||
import { isNavigatorGlobal, refuseNavigator } from "../builtins/navigator.js";
|
||||
import { wasiGuestPath } from "../../../wasi-paths.js";
|
||||
|
||||
/** A JavaScript binding with checked-dynamic storage because the program
|
||||
* stores values outside its declared type (analyzeJsWidenedBindings): the
|
||||
* checker's narrowing at a read does not describe the stored value. */
|
||||
function isJsWidenedBinding(lowerer: Lowerer, expr: ts.Identifier): boolean {
|
||||
if (lowerer.jsWidenedBindings.size === 0) return false;
|
||||
const symbol = lowerer.resolveValueSymbol(expr);
|
||||
return symbol !== null && symbol !== undefined && lowerer.jsWidenedBindings.has(symbol);
|
||||
}
|
||||
|
||||
export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: SrcLoc): IrExpr {
|
||||
if (lowerer.isStdlibGlobal(expr, "console")) {
|
||||
return { kind: "libCall", fn: "console.native", args: [], type: DYN, loc };
|
||||
@@ -266,6 +275,8 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
}
|
||||
return { kind: "varRef", localId: local.id, type: local.type, loc };
|
||||
}
|
||||
if (local.type.kind === "dyn" && isJsWidenedBinding(lowerer, expr))
|
||||
return { kind: "varRef", localId: local.id, type: local.type, loc };
|
||||
const narrowed = lowerer.maybeNarrow(
|
||||
{ kind: "varRef", localId: local.id, type: local.type, loc },
|
||||
expr,
|
||||
@@ -369,6 +380,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
if (use?.kind === "call") return runtimeOptionalReceiverRead(lowerer, expr, g, null, loc);
|
||||
return ref;
|
||||
}
|
||||
if (g.type.kind === "dyn" && isJsWidenedBinding(lowerer, expr)) return ref;
|
||||
return lowerer.maybeNarrow(ref, expr);
|
||||
}
|
||||
// `undefined` — tsc's intrinsic global (a local shadowing the name
|
||||
|
||||
@@ -16,7 +16,7 @@ import { readFileSync, realpathSync } from "node:fs";
|
||||
import { resolveFrom } from "../../node-islands/resolve.js";
|
||||
import { dirname, join } from "node:path";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import type { NodeApiMember } from "../node-api-import.js";
|
||||
import { PoisonError, newFnCtx, type Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
@@ -458,6 +458,17 @@ function islandBindingOf(lowerer: Lowerer, ident: ts.Identifier): IslandBinding
|
||||
const name = decl.propertyName?.text ?? decl.name.text;
|
||||
binding = name === "default" ? "default" : { name };
|
||||
} else {
|
||||
// `const { name } = require("pkg")`: a member of module.exports, read
|
||||
// at each use (shortcut: not copied once at the declaration).
|
||||
const property = requiredPropertyOf(decl);
|
||||
if (property !== null && loadsAtStatement(property.declaration)) {
|
||||
const spec = requireInitSpecOf(property.declaration);
|
||||
if (spec === null) return null;
|
||||
const file = ts.getSourceFileOfNode(decl).fileName;
|
||||
const path = islandModuleFile(islands, file, spec, "require");
|
||||
const module = path === null ? undefined : lowerer.islandModules.get(path);
|
||||
return module === undefined ? null : { kind: "named", module, name: property.name };
|
||||
}
|
||||
// `const x = require("pkg")` (a CommonJS module of a native package
|
||||
// requiring an island package): x is module.exports.
|
||||
const spec = requireSpecOf(decl);
|
||||
@@ -634,8 +645,41 @@ function undeclaredHandleType(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
return containerType(lowerer, type);
|
||||
}
|
||||
|
||||
/** Builtin classes whose native representation is not a JavaScript object
|
||||
* (URLSearchParams, Headers, Request, …): an island instance of one has no
|
||||
* native counterpart to convert into, so it stays a handle. */
|
||||
const BUILTIN_HANDLE_CLASSES: ReadonlySet<string> = new Set([
|
||||
"URL",
|
||||
"URLSearchParams",
|
||||
"Headers",
|
||||
"Request",
|
||||
"Response",
|
||||
"FormData",
|
||||
"Blob",
|
||||
"File",
|
||||
"AbortController",
|
||||
"AbortSignal",
|
||||
"ReadableStream",
|
||||
"WritableStream",
|
||||
"TransformStream",
|
||||
"TextEncoder",
|
||||
"TextDecoder",
|
||||
]);
|
||||
|
||||
/** Whether `type` is one of BUILTIN_HANDLE_CLASSES as declared by the
|
||||
* standard library or @types/node (not a program class of the same name). */
|
||||
function builtinHandleClass(type: ts.Type): boolean {
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol === undefined || !BUILTIN_HANDLE_CLASSES.has(String(symbol.escapedName))) return false;
|
||||
return (
|
||||
symbol.declarations.length > 0 &&
|
||||
symbol.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)
|
||||
);
|
||||
}
|
||||
|
||||
/** Arrays, tuples, Maps and Sets (every arm of a union, nullish arms
|
||||
* aside): the object types an island hands back without declaring them. */
|
||||
* aside), and the builtin classes of builtinHandleClass: the object types
|
||||
* an island hands back without declaring them. */
|
||||
function containerType(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
if (type.isUnionType())
|
||||
return ts
|
||||
@@ -648,7 +692,7 @@ function containerType(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
|
||||
if (lowerer.checker.isArrayType(type) || lowerer.checker.isTupleType(type)) return true;
|
||||
const name = type.getSymbol()?.escapedName;
|
||||
return name === "Map" || name === "Set";
|
||||
return name === "Map" || name === "Set" || builtinHandleClass(type);
|
||||
}
|
||||
|
||||
/** A member read, call, index or `new` whose receiver or callee is an
|
||||
@@ -1490,6 +1534,20 @@ function islandReceiver(
|
||||
return { value: lowerer.coerceToExpected(lowerer.lowerExpr(receiver), FOREIGN) };
|
||||
}
|
||||
|
||||
/** Whether `require(spec)` in `file` loads an island module. */
|
||||
export function isIslandRequireSpec(lowerer: Lowerer, file: string, spec: string): boolean {
|
||||
const islands = lowerer.nodeIslands;
|
||||
return islands !== null && islandModuleFile(islands, file, spec, "require") !== null;
|
||||
}
|
||||
|
||||
/** `require("<island pkg>")` in a CommonJS module, at a call no other
|
||||
* lowering claimed (inside a function, or as a subexpression): the island
|
||||
* module loads at the call. Null when the package is not an island. */
|
||||
export function lowerIslandRequireCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
if (lowerer.nodeIslands === null) return null;
|
||||
return lowerIslandLoadCall(lowerer, call, locOf(call)) ?? null;
|
||||
}
|
||||
|
||||
/** `require("pkg")` (the global require, anywhere) and `import("pkg")` of
|
||||
* an island module: load it at the call. Undefined: not such a call. */
|
||||
function lowerIslandLoadCall(
|
||||
@@ -1837,12 +1895,84 @@ function plainLiteral(node: ts.ObjectLiteralExpression | ts.ArrayLiteralExpressi
|
||||
/** The specifier of `x = require("spec")` (a variable declaration binding
|
||||
* an identifier to a literal require), or null. */
|
||||
function requireSpecOf(decl: ts.Node): string | null {
|
||||
if (
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
decl.initializer === undefined
|
||||
)
|
||||
return null;
|
||||
if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null;
|
||||
return requireInitSpecOf(decl);
|
||||
}
|
||||
|
||||
/** The property a destructured CommonJS require binds: `name` in
|
||||
* `const { name } = require("spec")` or `const { name: local } = …`, with
|
||||
* the declaration. Rest elements, defaults and nested patterns are null. */
|
||||
function requiredPropertyOf(
|
||||
decl: ts.Node,
|
||||
): { name: string; declaration: ts.VariableDeclaration } | null {
|
||||
if (!ts.isBindingElement(decl) || decl.dotDotDotToken || decl.initializer) return null;
|
||||
if (!ts.isIdentifier(decl.name)) return null;
|
||||
const pattern = decl.parent;
|
||||
if (pattern === undefined || !ts.isObjectBindingPattern(pattern)) return null;
|
||||
const declaration = pattern.parent;
|
||||
if (declaration === undefined || !ts.isVariableDeclaration(declaration)) return null;
|
||||
const key = decl.propertyName;
|
||||
const name =
|
||||
key === undefined
|
||||
? decl.name.text
|
||||
: ts.isIdentifier(key) || ts.isStringLiteral(key)
|
||||
? key.text
|
||||
: null;
|
||||
return name === null ? null : { name, declaration };
|
||||
}
|
||||
|
||||
/** Whether every element of `const { … } = require("<island>")` binds a
|
||||
* plain member (requiredPropertyOf): only then do the names read the
|
||||
* module global; other patterns keep the ordinary lowering. */
|
||||
function islandPatternBindable(pattern: ts.ObjectBindingPattern): boolean {
|
||||
return pattern.elements.nodes.every((e) => requiredPropertyOf(e) !== null);
|
||||
}
|
||||
|
||||
/** Whether the top-level declaration `decl` of a JavaScript module loads
|
||||
* its island module at the statement (lowerIslandRequireAtStatement)
|
||||
* rather than in the module's %init prelude: `const`/`let` declarations
|
||||
* binding an identifier or a plain object pattern. Reads before the
|
||||
* statement are TDZ errors in Node, so the binding is never observed
|
||||
* unassigned. `var` keeps the prelude load (shortcut: a hoisted `var` read
|
||||
* before its statement sees the module, where Node sees undefined). */
|
||||
function loadsAtStatement(decl: ts.VariableDeclaration): boolean {
|
||||
const list = decl.parent;
|
||||
if (list === undefined || !ts.isVariableDeclarationList(list)) return false;
|
||||
if ((list.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)) === 0) return false;
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return false;
|
||||
if (ts.isIdentifier(decl.name)) return true;
|
||||
return ts.isObjectBindingPattern(decl.name) && islandPatternBindable(decl.name);
|
||||
}
|
||||
|
||||
/** The top-level statement `const x = require("<island>")` or `const { a,
|
||||
* b: c } = require("<island>")` of a JavaScript module: assigns the island
|
||||
* module (deferred like any top-level require) to its module global at the
|
||||
* statement's position, so writes before it (`process.env.NODE_ENV = …`)
|
||||
* are visible to the module's evaluation, as in Node; `x` is the module,
|
||||
* `a` and `c` read its members at each use (shortcut: not copied once at
|
||||
* the declaration). Null for any other declaration. */
|
||||
export function lowerIslandRequireAtStatement(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): IrStmt | null {
|
||||
const islands = lowerer.nodeIslands;
|
||||
if (islands === null || !loadsAtStatement(decl)) return null;
|
||||
const spec = requireInitSpecOf(decl);
|
||||
if (spec === null) return null;
|
||||
const path = islandModuleFile(islands, ts.getSourceFileOfNode(decl).fileName, spec, "require");
|
||||
const module = path === null ? undefined : lowerer.islandModules.get(path);
|
||||
if (path === null || module === undefined) return null;
|
||||
const loc = locOf(decl);
|
||||
const load = runsAtModuleEvaluation(decl)
|
||||
? deferIslandModule(path, loc)
|
||||
: loadIslandModule(path, loc);
|
||||
return { kind: "assign", localId: module.global.id, value: load, loc };
|
||||
}
|
||||
|
||||
/** The specifier of a variable declaration initialized with a literal
|
||||
* `require("spec")`, whatever its binding name or pattern, or null. */
|
||||
function requireInitSpecOf(decl: ts.VariableDeclaration): string | null {
|
||||
if (decl.initializer === undefined) return null;
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (
|
||||
@@ -1859,7 +1989,8 @@ function requireSpecOf(decl: ts.Node): string | null {
|
||||
/** Top-level `const x = require("pkg")` statements of island packages in a
|
||||
* compiled module (a native package requiring an island package): load the
|
||||
* island module in the module's %init prelude (shortcut: before the body,
|
||||
* not at the statement), deferred exactly like a static import (scope-B
|
||||
* not at the statement; JavaScript `const`/`let` declarations load at the
|
||||
* statement instead, loadsAtStatement), deferred exactly like a static import (scope-B
|
||||
* NI-7d: the build decides from the probe, stubs stand in until first use). */
|
||||
export function collectIslandRequires(lowerer: Lowerer, sf: ts.SourceFile): void {
|
||||
const islands = lowerer.nodeIslands;
|
||||
@@ -1867,6 +1998,18 @@ export function collectIslandRequires(lowerer: Lowerer, sf: ts.SourceFile): void
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (!ts.isVariableStatement(stmt)) continue;
|
||||
for (const decl of stmt.declarationList.declarations.nodes) {
|
||||
// JavaScript `const`/`let` requires: the module global exists from
|
||||
// here on; the statement itself loads the module at its position
|
||||
// (lowerIslandRequireAtStatement).
|
||||
if (loadsAtStatement(decl)) {
|
||||
const spec = requireInitSpecOf(decl);
|
||||
const path = spec === null ? null : islandModuleFile(islands, sf.fileName, spec, "require");
|
||||
if (path !== null) {
|
||||
lowerer.usesNodeIsland = true;
|
||||
islandModuleGlobal(lowerer, path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const spec = requireSpecOf(decl);
|
||||
if (spec === null) continue;
|
||||
const path = islandModuleFile(islands, sf.fileName, spec, "require");
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
|
||||
import { islandHandleBinding, lowerIslandExpr, lowerIslandSubclass } from "./lower-node-island.js";
|
||||
import {
|
||||
islandHandleBinding,
|
||||
lowerIslandExpr,
|
||||
isIslandRequireSpec,
|
||||
lowerIslandRequireAtStatement,
|
||||
lowerIslandSubclass,
|
||||
} from "./lower-node-island.js";
|
||||
import { isIslandSubclassDecl } from "../../node-islands/boundary.js";
|
||||
import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property-access.js";
|
||||
import { hostsAll } from "./expressions/hosted-narrowing.js";
|
||||
@@ -1868,6 +1874,10 @@ function lowerVarDeclarator(
|
||||
const spec = requireSpecOf(decl.initializer)!;
|
||||
if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false);
|
||||
if (ts.isSourceFile(stmt.parent)) {
|
||||
// `const x = require("<island>")` and `const { name } = …`: the
|
||||
// island module loads here, at the statement (lower-node-island.ts).
|
||||
const island = lowerIslandRequireAtStatement(lowerer, decl);
|
||||
if (island !== null) return [island];
|
||||
const init = lowerer.requireInitStmt(spec, decl);
|
||||
const out: IrStmt[] = init ? [init] : [];
|
||||
// Nested patterns destructure the named export's value right after
|
||||
@@ -1892,6 +1902,12 @@ function lowerVarDeclarator(
|
||||
// same routing top-level requires use, and no statement remains.
|
||||
// Relative requires keep the fence: their module-init-at-position
|
||||
// semantics needs storage this model doesn't represent.
|
||||
// A nested require of an island package is an ordinary binding of the
|
||||
// island module, loaded at the statement (lower-node-island.ts).
|
||||
if (isIslandRequireSpec(lowerer, ts.getSourceFileOfNode(decl).fileName, spec)) {
|
||||
const lowered = lowerer.lowerVarDecl(decl, isLet);
|
||||
return lowered ? [lowered] : [];
|
||||
}
|
||||
if (canonicalBuiltinModule(spec) !== null) {
|
||||
return canonicalBuiltinModule(spec) === "punycode"
|
||||
? [lowerer.punycodeRequireWarning(decl)]
|
||||
@@ -5451,6 +5467,12 @@ export function lowerVarDecl(
|
||||
// Promise.prototype is an ordinary object the checker types Promise<any>.
|
||||
if (!decl.type && !hasJsTypeAnnotation(decl) && isPromisePrototypeRead(lowerer, decl.initializer))
|
||||
type = DYN;
|
||||
// A JavaScript binding assigned values outside its declared type
|
||||
// (analyzeJsWidenedBindings in lowerer.ts) holds them as checked-dynamic.
|
||||
{
|
||||
const symbol = lowerer.resolveValueSymbol(decl.name);
|
||||
if (symbol && lowerer.jsWidenedBindings.has(symbol)) type = DYN;
|
||||
}
|
||||
// A typed-arguments function expression (typed-arguments.ts): the
|
||||
// checker types the binding variadic, but its value has the fixed
|
||||
// signature every use asserts.
|
||||
|
||||
@@ -2353,6 +2353,10 @@ export class Lowerer {
|
||||
* prepass. This includes explicit annotations: unchecked TS annotations do
|
||||
* not prove that an array property read produced a value. */
|
||||
readonly runtimeOptionalBindingTypes = new Map<ts.Symbol, IrType>();
|
||||
/** JavaScript bindings assigned values outside their declared type
|
||||
* (analyzeJsWidenedBindings): checked-dynamic storage, and reads keep the
|
||||
* stored value instead of narrowing it to the checker's type. */
|
||||
readonly jsWidenedBindings = new Set<ts.Symbol>();
|
||||
/** A pipeline() function stage's `source` parameter: the IR type of what
|
||||
* the pipeline passes it (the previous stage as an async iterable), set
|
||||
* while the stage lowers. Parameter typing and `for await` heads read
|
||||
@@ -5036,6 +5040,49 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
|
||||
/** A JavaScript `let`/`var` binding assigned a value its declared type
|
||||
* does not admit (`let t = parseInt(s); if (isNaN(t)) t = undefined`):
|
||||
* TypeScript types the binding from its initializer and reports the
|
||||
* assignment (TS2322), which JavaScript files relax, so the binding takes
|
||||
* checked-dynamic storage and holds whatever the program stores. JSDoc
|
||||
* type annotations keep their declared storage. */
|
||||
analyzeJsWidenedBindings(parts: FileParts[]): void {
|
||||
for (const { sf } of parts) {
|
||||
if (!isJsSourceFileName(sf.fileName) || sf.isDeclarationFile) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
!ts.isBinaryExpression(node) ||
|
||||
node.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!ts.isIdentifier(node.left)
|
||||
)
|
||||
return undefined;
|
||||
const symbol = this.resolveValueSymbol(node.left);
|
||||
if (!symbol || this.runtimeOptionalBindingTypes.has(symbol)) return undefined;
|
||||
const decl = symbol.valueDeclaration;
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
!isJsSourceFileName(ts.getSourceFileOfNode(decl).fileName) ||
|
||||
hasJsTypeAnnotation(decl) ||
|
||||
decl.parent === undefined ||
|
||||
!ts.isVariableDeclarationList(decl.parent) ||
|
||||
(decl.parent.flags & ts.NodeFlags.Const) !== 0
|
||||
)
|
||||
return undefined;
|
||||
const declared = this.checker.getTypeOfSymbol(symbol);
|
||||
if (declared.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return undefined;
|
||||
const assigned = this.checker.getTypeAtLocation(node.right);
|
||||
if (this.checker.isTypeAssignableTo(assigned, declared)) return undefined;
|
||||
this.runtimeOptionalBindingTypes.set(symbol, DYN);
|
||||
this.jsWidenedBindings.add(symbol);
|
||||
const global = this.globalsBySymbol.get(symbol);
|
||||
if (global) global.type = DYN;
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
analyzeRuntimeOptionalArrayReads(parts: FileParts[]): void {
|
||||
const provenance = RuntimeOptionalProvenance.fromEnvironment();
|
||||
// The site whose value the current promotion follows (debug only).
|
||||
@@ -7517,6 +7564,7 @@ export class Lowerer {
|
||||
this.analyzeRuntimeOptionalArrayReads(parts);
|
||||
this.analyzeDateJsonResults(parts);
|
||||
this.analyzeGuardedRecordReadGlobals(parts);
|
||||
this.analyzeJsWidenedBindings(parts);
|
||||
});
|
||||
// Decorated classes analyze AFTER the whole collection pass: a
|
||||
// decorator's return type may name a subclass declared below the
|
||||
@@ -7947,6 +7995,7 @@ export class Lowerer {
|
||||
this.analyzeRuntimeOptionalArrayReads(parts);
|
||||
this.analyzeDateJsonResults(parts);
|
||||
this.analyzeGuardedRecordReadGlobals(parts);
|
||||
this.analyzeJsWidenedBindings(parts);
|
||||
});
|
||||
// Decorated classes analyze post-collection here too: the %init seeds
|
||||
// lower the decoration calls, whose edges (decorator bodies, construct
|
||||
|
||||
@@ -230,7 +230,7 @@ function detectAutoPackages(
|
||||
// without the declaration eligibility bar.
|
||||
const owner = npmStaticPackageOfPath(sf.fileName);
|
||||
const viaTransitive = owner !== null && transitive.has(owner);
|
||||
const edges: { spec: string; loc: SrcLoc }[] = [];
|
||||
const edges: { spec: string; loc: SrcLoc; nested?: true }[] = [];
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (
|
||||
(isImportDeclaration(stmt) || isExportDeclaration(stmt)) &&
|
||||
@@ -248,6 +248,33 @@ function detectAutoPackages(
|
||||
for (const req of requiresOf(stmt)) edges.push({ spec: req.spec, loc: locOf(req.node) });
|
||||
}
|
||||
}
|
||||
// A program file's literal require() calls outside top-level require
|
||||
// statements (inside functions, in expressions) name packages the
|
||||
// program loads at run time too. They count only to refuse a package
|
||||
// that ships no declarations, which then runs in a Node island; any
|
||||
// other package keeps the top-level edge rule.
|
||||
if (
|
||||
mode === "lib" &&
|
||||
!viaTransitive &&
|
||||
owner === null &&
|
||||
!sf.fileName.includes("/node_modules/") &&
|
||||
sf.text.includes("require(")
|
||||
) {
|
||||
const topLevel = new Set(edges.map((e) => `${e.spec}@${e.loc.start}`));
|
||||
for (const call of moduleSourceCandidates(load.program, sf).calls) {
|
||||
const arg = call.arguments.nodes[0];
|
||||
if (
|
||||
arg === undefined ||
|
||||
!isStringLiteralLike(arg) ||
|
||||
!isIdentifier(call.expression) ||
|
||||
call.expression.text !== "require"
|
||||
)
|
||||
continue;
|
||||
const loc = locOf(call);
|
||||
if (topLevel.has(`${arg.text}@${loc.start}`)) continue;
|
||||
edges.push({ spec: arg.text, loc, nested: true });
|
||||
}
|
||||
}
|
||||
// Transitive selection counts every literal require() and import()
|
||||
// anywhere in the package, nested ones included.
|
||||
if (viaTransitive) {
|
||||
@@ -262,7 +289,7 @@ function detectAutoPackages(
|
||||
edges.push({ spec: arg.text, loc: locOf(call) });
|
||||
}
|
||||
}
|
||||
for (const { spec, loc } of edges) {
|
||||
for (const { spec, loc, nested } of edges) {
|
||||
if (load.externalTypes?.has(spec)) continue;
|
||||
// scriptc's own packages are not npm dependencies: contract types are
|
||||
// declarations only, and @scriptc/threads and @scriptc/experimental
|
||||
@@ -304,6 +331,13 @@ function detectAutoPackages(
|
||||
? resolveBareModule(sf.fileName, spec, "js-only")
|
||||
: null;
|
||||
const runtimePkg = js !== null && js.packageName !== npm?.packageName ? js.packageName : null;
|
||||
// Nested requires only refuse packages without own declarations.
|
||||
if (
|
||||
nested === true &&
|
||||
!(npm === null && runtimePkg !== null) &&
|
||||
!(npm !== null && !/\.d\.(ts|mts|cts)$/.test(npm.typesFile))
|
||||
)
|
||||
continue;
|
||||
if (npm === null || runtimePkg !== null) {
|
||||
if (mode !== "lib" || runtimePkg === null || judged!.has(runtimePkg)) continue;
|
||||
judged!.add(runtimePkg);
|
||||
|
||||
@@ -132,6 +132,16 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet<number> = new Set([
|
||||
// binding types as any and reached uses fence instead of passing unchecked
|
||||
2307,
|
||||
2792,
|
||||
// a write to a property declarations mark readonly (2540): `readonly` is
|
||||
// a type annotation that Node never sees (Next.js's types mark
|
||||
// process.env.NODE_ENV readonly; its server.js assigns it); the write
|
||||
// lowers like any other and keeps its runtime behavior
|
||||
2540,
|
||||
// a resolved JavaScript module without declarations (7016): Node loads
|
||||
// it like any other module; deployment trees such as Next.js standalone
|
||||
// output ship runtime files only, so the binding types as any and its
|
||||
// uses take the dynamic paths, as for 2307
|
||||
7016,
|
||||
// useUnknownInCatchVariables' annotation demand ("'e' is of type
|
||||
// 'unknown'", "Object is of type 'unknown'"): a JS catch clause CANNOT
|
||||
// annotate its variable, so the demand is unsatisfiable in source —
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6880-node-island-cjs-entry/main.js"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1010",
|
||||
"message": "the 'envkit' module is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/corpus/6880-node-island-cjs-entry/main.js",
|
||||
"start": 628,
|
||||
"end": 645
|
||||
},
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"code": "SC1010",
|
||||
"message": "the 'envkit' module is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/corpus/6880-node-island-cjs-entry/main.js",
|
||||
"start": 652,
|
||||
"end": 678
|
||||
},
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"code": "SC1010",
|
||||
"message": "the 'startkit' module is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/corpus/6880-node-island-cjs-entry/main.js",
|
||||
"start": 685,
|
||||
"end": 745
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6881-js-let-widened-assignment.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6882-node-island-nested-require/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6883-node-island-builtin-handles/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6884-js-readonly-declared-write/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-9
@@ -3,15 +3,6 @@
|
||||
"<repo>/tests/corpus/readable-stream-3/main.cjs"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Could not find a declaration file for module 'readable-stream'. '<repo>/tests/corpus/readable-stream-3/node_modules/readable-stream/readable.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream';`",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/corpus/readable-stream-3/main.cjs",
|
||||
"start": 554,
|
||||
"end": 571
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC1010",
|
||||
"message": "the 'readable-stream' module is not supported yet",
|
||||
|
||||
-36
@@ -3,42 +3,6 @@
|
||||
"<repo>/tests/corpus/readable-stream-disable/main.cjs"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Could not find a declaration file for module 'readable-stream'. '<repo>/tests/corpus/readable-stream-disable/node_modules/readable-stream/readable.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream';`",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/corpus/readable-stream-disable/main.cjs",
|
||||
"start": 581,
|
||||
"end": 598
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Could not find a declaration file for module 'readable-stream/transform'. '<repo>/tests/corpus/readable-stream-disable/node_modules/readable-stream/transform.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream/transform';`",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/corpus/readable-stream-disable/main.cjs",
|
||||
"start": 625,
|
||||
"end": 652
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Could not find a declaration file for module 'readable-stream/passthrough'. '<repo>/tests/corpus/readable-stream-disable/node_modules/readable-stream/passthrough.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream/passthrough';`",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/corpus/readable-stream-disable/main.cjs",
|
||||
"start": 681,
|
||||
"end": 710
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Could not find a declaration file for module 'readable-stream/writable'. '<repo>/tests/corpus/readable-stream-disable/node_modules/readable-stream/writable.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream/writable';`",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/corpus/readable-stream-disable/main.cjs",
|
||||
"start": 736,
|
||||
"end": 762
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC1010",
|
||||
"message": "the 'readable-stream' module is not supported yet",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// @requires-libnode
|
||||
// The shape of a Next.js standalone server.js: a CommonJS entry that writes
|
||||
// process.env, then requires packages that ship no declarations (TS7016 in
|
||||
// a JavaScript file is relaxed), destructures one of them, and calls into
|
||||
// it. The packages run in Node islands. Each require loads its island module
|
||||
// at the statement, so the environment writes before it are visible to the
|
||||
// module's evaluation, as in Node.
|
||||
const path = require('path')
|
||||
|
||||
console.log('entry start')
|
||||
process.env.ISLAND_MODE = 'production'
|
||||
process.env.ISLAND_CONFIG = JSON.stringify({ name: path.basename('/srv/app'), port: 3000 })
|
||||
|
||||
require('envkit')
|
||||
const envkit = require('envkit')
|
||||
const { startServer, version: startVersion } = require('startkit')
|
||||
|
||||
console.log(envkit.seen())
|
||||
console.log('loads', envkit.loads())
|
||||
console.log('startkit', startVersion, typeof startServer)
|
||||
|
||||
const config = JSON.parse(process.env.ISLAND_CONFIG)
|
||||
startServer({ name: config.name, port: config.port })
|
||||
.then((message) => {
|
||||
console.log(message)
|
||||
return startServer({ name: 'bad', port: -1 })
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('caught', err.message)
|
||||
})
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Ships no declarations, like the traced node_modules of a Next.js
|
||||
// standalone build. Records the environment its evaluation saw.
|
||||
const seenMode = process.env.ISLAND_MODE;
|
||||
const seenConfig = process.env.ISLAND_CONFIG;
|
||||
globalThis.__envkitLoads = (globalThis.__envkitLoads || 0) + 1;
|
||||
exports.seen = () => `envkit saw ISLAND_MODE=${seenMode}, config=${seenConfig}`;
|
||||
exports.loads = () => globalThis.__envkitLoads;
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "envkit", "version": "1.0.0", "main": "index.js" }
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Its evaluation has effects (a timer), so the island loads eagerly.
|
||||
const seenMode = process.env.ISLAND_MODE;
|
||||
setTimeout(() => {}, 0);
|
||||
exports.startServer = async function startServer(opts) {
|
||||
if (opts.port < 0) throw new Error(`bad port ${opts.port}`);
|
||||
return `started ${opts.name} on ${opts.port} (startkit saw ISLAND_MODE=${seenMode}, now ${process.env.ISLAND_MODE})`;
|
||||
};
|
||||
exports.version = "1.0.0";
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "startkit", "version": "1.0.0", "main": "index.js" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "island-cjs-entry", "private": true }
|
||||
@@ -0,0 +1,34 @@
|
||||
// A JavaScript binding whose later assignments store values its
|
||||
// initializer's type does not admit (TS2322, relaxed in JavaScript files):
|
||||
// the binding holds whatever the program stores, as in Node. Next.js's
|
||||
// standalone server.js does this with KEEP_ALIVE_TIMEOUT.
|
||||
let keepAliveTimeout = parseInt(process.env.SCRIPTC_UNSET_TIMEOUT, 10)
|
||||
if (Number.isNaN(keepAliveTimeout) || !Number.isFinite(keepAliveTimeout) || keepAliveTimeout < 0) {
|
||||
keepAliveTimeout = undefined
|
||||
}
|
||||
console.log(keepAliveTimeout, typeof keepAliveTimeout)
|
||||
const opts = { keepAliveTimeout }
|
||||
console.log(JSON.stringify(opts), 'keepAliveTimeout' in opts, opts.keepAliveTimeout === undefined)
|
||||
|
||||
let label = 'none'
|
||||
function relabel(n) {
|
||||
label = n
|
||||
}
|
||||
relabel(42)
|
||||
console.log(label, typeof label)
|
||||
|
||||
function show(raw) {
|
||||
let value = Number(raw)
|
||||
if (Number.isNaN(value)) value = null
|
||||
console.log('value', value, value === null)
|
||||
}
|
||||
show('12')
|
||||
show('x')
|
||||
|
||||
var counter = 0
|
||||
counter = counter + 1
|
||||
console.log('counter', counter)
|
||||
|
||||
let port = parseInt('8080', 10)
|
||||
if (port > 65535) port = undefined
|
||||
console.log('port', port, typeof port)
|
||||
@@ -0,0 +1,26 @@
|
||||
// @requires-libnode
|
||||
// require() of a Node island package inside functions of a CommonJS
|
||||
// entry: the module loads at the first call that requires it (after the
|
||||
// environment write before that call), later calls get the same module.
|
||||
'use strict'
|
||||
|
||||
function viaExpression(name) {
|
||||
return require('lazykit').greet(name)
|
||||
}
|
||||
|
||||
function viaBinding(name) {
|
||||
const lazykit = require('lazykit')
|
||||
return `${lazykit.greet(name)} (${lazykit.loads()} load)`
|
||||
}
|
||||
|
||||
const lazy = {
|
||||
get kit() {
|
||||
return require('lazykit')
|
||||
},
|
||||
}
|
||||
|
||||
console.log('start')
|
||||
process.env.ISLAND_PHASE = 'after-start'
|
||||
console.log(viaExpression('first'))
|
||||
console.log(viaBinding('second'))
|
||||
console.log(lazy.kit.greet('getter'), lazy.kit === require('lazykit'))
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Ships no declarations. Counts its evaluations: Node evaluates it once.
|
||||
globalThis.__lazykitLoads = (globalThis.__lazykitLoads || 0) + 1;
|
||||
console.log('lazykit evaluated, ISLAND_PHASE =', process.env.ISLAND_PHASE);
|
||||
exports.greet = (name) => `hello ${name}`;
|
||||
exports.loads = () => globalThis.__lazykitLoads;
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "lazykit", "version": "1.0.0", "main": "index.js" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "island-nested-require", "private": true }
|
||||
@@ -0,0 +1,21 @@
|
||||
// @requires-libnode
|
||||
// Island values typed with builtin classes (URLSearchParams, Headers, URL)
|
||||
// stay island handles: their members and methods run in the island, and
|
||||
// passing them back hands the island its own object.
|
||||
import { RequestLike, describeParams, makeParams } from "urlkit";
|
||||
|
||||
const request = new RequestLike("http://localhost/api/products?tag=software&limit=2", {
|
||||
"x-trace": "abc",
|
||||
});
|
||||
const tag = request.searchParams.get("tag") ?? "none";
|
||||
const missing = request.searchParams.get("missing") ?? "none";
|
||||
console.log("tag", tag, "missing", missing, "has limit", request.searchParams.has("limit"));
|
||||
console.log("pathname", request.url.pathname, "host", request.url.host);
|
||||
console.log("trace", request.headers.get("x-trace"), "absent", request.headers.get("x-absent"));
|
||||
|
||||
const params = request.searchParams;
|
||||
params.append("page", "3");
|
||||
console.log("described", describeParams(params));
|
||||
console.log("round trip", describeParams(makeParams("a=1&b=two")));
|
||||
console.log("same object", params === request.searchParams);
|
||||
console.log("size", params.size);
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export declare class RequestLike {
|
||||
constructor(url: string, headers?: Record<string, string>);
|
||||
readonly url: URL;
|
||||
readonly headers: Headers;
|
||||
get searchParams(): URLSearchParams;
|
||||
}
|
||||
export declare function describeParams(params: URLSearchParams): string;
|
||||
export declare function makeParams(query: string): URLSearchParams;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// An island class whose members are builtin class instances (the shape of
|
||||
// Next.js's NextRequest.nextUrl.searchParams).
|
||||
export class RequestLike {
|
||||
constructor(url, headers = {}) {
|
||||
this.url = new URL(url);
|
||||
this.headers = new Headers(headers);
|
||||
}
|
||||
get searchParams() {
|
||||
return this.url.searchParams;
|
||||
}
|
||||
}
|
||||
export function describeParams(params) {
|
||||
return [...params.entries()].map(([k, v]) => `${k}=${v}`).join("&");
|
||||
}
|
||||
export function makeParams(query) {
|
||||
return new URLSearchParams(query);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "urlkit", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "island-builtin-handles", "private": true, "type": "module", "scriptc": { "packages": { "urlkit": "node" } } }
|
||||
@@ -0,0 +1,7 @@
|
||||
// Declarations that mark a member readonly, as Next.js's global types do
|
||||
// for process.env.NODE_ENV.
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
readonly APP_STAGE?: "development" | "production";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/// <reference path="./env.d.ts" />
|
||||
// A JavaScript entry writes a member its declarations mark readonly
|
||||
// (TS2540, relaxed in JavaScript files): `readonly` is a type annotation,
|
||||
// and the write behaves as it does in Node.
|
||||
console.log('before', process.env.APP_STAGE)
|
||||
process.env.APP_STAGE = 'production'
|
||||
console.log('after', process.env.APP_STAGE, typeof process.env.APP_STAGE)
|
||||
delete process.env.APP_STAGE
|
||||
console.log('deleted', process.env.APP_STAGE)
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3300, "san": 23000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 5400 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3300, "san": 23000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3300, "san": 23000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 5400 }
|
||||
Reference in New Issue
Block a user