Vercel CLI source ladder: five frontend fixes from the typecheck gate

- Bare '.' and '..' specifiers resolve as relative directory imports
- TS default imports of supported builtins pass under the project's interop knobs
- Workspace packages register eagerly before the tsc gate; their shipped JS never gates
- Benign-cycle admission accepts d.ts-rooted callable arguments in the init window
- npm-static auto probes from the importing file's realm; ladder fixtures renumbered 2390/2391
This commit is contained in:
Chris Tate
2026-07-22 21:35:46 -05:00
36 changed files with 424 additions and 80 deletions
+1
View File
@@ -5,6 +5,7 @@ node_modules/
!tests/fixtures/npm/node_modules/
!tests/corpus/*/node_modules/
!tests/fixtures/npm-static/node_modules/
!tests/fixtures/npm-static/nested/inner/node_modules/
!tests/fixtures/npm/store/*/node_modules/
!tests/fixtures/npm/cases/*/node_modules/
!tests/fixtures/provenance/node_modules/
@@ -7,7 +7,7 @@ import type { Lowerer } from "./lowerer.js";
import { dirname as dirnamePath, resolve as resolvePath } from "node:path";
import { NpmGraphBuilder, packageNameOfPath, probeNodeImportRefusal } from "../npm.js";
import { isNpmStaticPackage } from "../npm-static.js";
import { isJsSourceFileName } from "../shared.js";
import { isJsSourceFileName, isRelativeSpecifier } from "../shared.js";
import { cjsExportAssignmentOf, cjsExportDiscardReason, isCjsJsFile, isJsSourceFile, isRequireStatement, locOf, orderedImportsOf, resolveImport, resolveNpmImport } from "../program.js";
import { invalidJsonModuleDiag, npmEmbedFailedDiag, requiresDynamicImportDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
import { BOOL, DYN, IrClassDef, IrExpr, IrFunction, IrGlobal, IrRecordShape, IrStmt, IrUnionDef, JSVAL, RUNTIME_ERROR_CLASSES, STRING, SrcLoc, VOID, canConvertToDyn, isUnitType } from "../../ir/nodes.js";
@@ -75,7 +75,7 @@ export interface FileParts {
sf: ts.SourceFile,
spec: string,
): ts.SourceFile | null {
if (!spec.startsWith("./") && !spec.startsWith("../") && !spec.startsWith("/")) return null;
if (!isRelativeSpecifier(spec) && !spec.startsWith("/")) return null;
const dep = resolveImport(program, sf, spec);
if (!dep || dep.isDeclarationFile) return null;
if (dep.fileName.endsWith(".json") || dep.fileName.endsWith(".cts")) return null;
@@ -240,7 +240,7 @@ export interface FileParts {
// and node_modules files never lower as program modules — the
// import silently dropped).
const relPkg =
npm === null && (spec.startsWith("./") || spec.startsWith("../"))
npm === null && isRelativeSpecifier(spec)
? packageNameOfPath(resolvePath(dirnamePath(fp.sf.fileName), spec))
: null;
const relIsJs =
@@ -24,7 +24,7 @@ import { lowerEnumDeclaration } from "./lower-enums.js";
import { abstractPropertyDeclOf, aliasTypeofNarrows, probeLower, pureReemittable, symbolFieldInfo } from "./lower-exprs.js";
import { UNSUPPORTED, checkerPanicDiag, isCheckerPanic, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
import { isUnitOnlyTsType, unitOnlyUnion } from "../types.js";
import { canonicalBuiltinModule } from "../shared.js";
import { canonicalBuiltinModule, isRelativeSpecifier } from "../shared.js";
import { probeNodeRequireRefusal } from "../npm.js";
/** `const X = /* @__PURE__ *\/ makeX()` at a module's top level: every
@@ -3017,7 +3017,7 @@ export function lowerVarDecl(L: Lowerer, decl: ts.VariableDeclaration, isLet: bo
if (isCjsJsFile(sf)) {
const spec = requireSpecOf(expr)!;
if (
!spec.startsWith("./") && !spec.startsWith("../") &&
!isRelativeSpecifier(spec) &&
canonicalBuiltinModule(spec) === null
) {
const refusal = probeNodeRequireRefusal(sf.fileName, spec);
@@ -12,6 +12,7 @@
* - Lexical scoping is resolved here: locals get function-unique ids
* ("x.0", "x.1" for shadowing); the IR is scope-flat.
*/
import { isRelativeSpecifier } from "../shared.js";
import * as ts from "../ts7/adapter.js";
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
import {
@@ -1236,7 +1237,7 @@ export class Lowerer {
return null;
}
const spec = importDecl.moduleSpecifier.text;
if (!spec.startsWith("./") && !spec.startsWith("../")) return null;
if (!isRelativeSpecifier(spec)) return null;
const dep = resolveImport(this.program, importDecl.getSourceFile(), spec);
if (!dep || !isJsSourceFile(dep) || isNodeEsmFile(dep)) return null;
return dep;
@@ -1263,7 +1264,7 @@ export class Lowerer {
return false;
}
const spec = requireSpecOf(decl.initializer);
if (spec === null || !(spec.startsWith("./") || spec.startsWith("../"))) return false;
if (spec === null || !isRelativeSpecifier(spec)) return false;
}
// SINGLE-VALUE exporters (`module.exports = Countdown` / `= double` /
// `= 42`): the requirer's binding IS the exported value, not a
@@ -1364,7 +1365,7 @@ export class Lowerer {
* specifier is not a relative program module (builtins load nothing;
* anything else kept its preflight fence). */
requireInitStmt(spec: string, node: ts.Node): IrStmt | null {
if (!spec.startsWith("./") && !spec.startsWith("../")) return null;
if (!isRelativeSpecifier(spec)) return null;
const dep = resolveImport(this.program, node.getSourceFile(), spec);
if (!dep || dep.fileName.endsWith(".json")) return null;
const initName = this.initNameOf.get(dep);
+111 -20
View File
@@ -63,11 +63,13 @@ import {
fallbackDtsPath,
isJsSourceFileName,
isNodeTypesPath,
isRelativeSpecifier,
JS_ANY_OPERATOR_CODES,
JS_RELAXED_TSC_CODES,
overridesDtsPath,
registerWorkspacePackage,
SUPPORTED_NODE_MODULES,
workspacePackageOfPath,
} from "./shared.js";
const BASE_OPTIONS: ts.Ts7CompilerOptions = {
@@ -883,11 +885,17 @@ function nonInertTopLevel7(program: ts.Program, sf: ts.SourceFile): ts.Node | nu
if (ts.isCallExpression(e) || ts.isNewExpression(e)) {
if (!dtsRooted(e.expression) || !ts.isIdentifier(chainRoot7(e.expression))) return false;
const args = e.arguments ?? [];
// A CALLABLE argument is admissible when it is itself dts-rooted:
// a builtin-owned function value (vercel's `promisify(fs.readFile)`
// at every cycle member's top level) is runtime-implemented — even
// if the builtin callee invokes it, no user code runs and no
// cluster binding is observable. Function literals and user
// callables keep the refusal.
return args.every(
(a) =>
inert(a) &&
!containsFunctionLike(a) &&
checker.getCallSignatures(checker.getTypeAtLocation(a)).length === 0,
(checker.getCallSignatures(checker.getTypeAtLocation(a)).length === 0 || dtsRooted(a)),
);
}
return false;
@@ -1006,7 +1014,7 @@ function resolveNpmImport7(
fromFileName: string,
specifier: string,
): { packageName: string; version?: string; typesFile: string } | null {
if (specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("node:")) {
if (isRelativeSpecifier(specifier) || specifier.startsWith("node:")) {
return null;
}
// --provenance-sources: a registered specifier is NOT an npm import —
@@ -1106,6 +1114,61 @@ function preflight7(load: LoadResult): {
const { program, entry } = load;
const diags: ScrDiagnostic[] = [...load.configDiags];
// Workspace-linked packages register BEFORE the tsc gate. Their files
// live at realpaths OUTSIDE node_modules (the monorepo-tool symlink
// shape), so nothing path-shaped marks them as npm surface — yet their
// shipped JS is the identical foreign-tsconfig story as node_modules JS
// (the island executes it under --dynamic; the author's own build
// checked it, the program's author cannot fix it). The lazy
// registration inside the import-fence walk below runs AFTER errorsOf,
// too late for the gate, so one pass over the program's import edges
// registers them up front: every bare specifier any program file loads —
// import/export declarations, dynamic import("literal") (tsgo chases
// those into the program too; vercel's CLI reaches @vercel/go exactly
// that way), require("literal") — resolved with the own resolver,
// workspace answers recorded.
{
const supportedBuiltins = new Set<string>(SUPPORTED_NODE_MODULES);
const probed = new Set<string>();
const probe = (fromFile: string, spec: string): void => {
if (
isRelativeSpecifier(spec) ||
spec.startsWith("#") ||
spec.startsWith("node:") ||
supportedBuiltins.has(spec)
) {
return;
}
const key = `${dirname(fromFile)} ${spec}`;
if (probed.has(key)) return;
probed.add(key);
const r = resolveBareModule(fromFile, spec);
if (r !== null && r.workspaceDir !== undefined) registerWorkspacePackage(r.packageName, r.workspaceDir);
};
for (const sf of program.getSourceFiles()) {
if (sf.isDeclarationFile || sf.fileName.endsWith(".json")) continue;
ts.walkPreorder(sf, (n) => {
if (
(ts.isImportDeclaration(n) || ts.isExportDeclaration(n)) &&
n.moduleSpecifier !== undefined &&
ts.isStringLiteral(n.moduleSpecifier)
) {
probe(sf.fileName, n.moduleSpecifier.text);
return "skip";
}
if (ts.isCallExpression(n)) {
const arg = n.arguments[0];
const isImportCall = n.expression.kind === ts.SyntaxKind.ImportKeyword;
const isRequireCall = ts.isIdentifier(n.expression) && n.expression.text === "require";
if ((isImportCall || isRequireCall) && arg !== undefined && ts.isStringLiteralLike(arg)) {
probe(sf.fileName, arg.text);
}
}
return undefined;
});
}
}
const toPassthrough = (d: ts.Diagnostic): ScrDiagnostic => {
const message = ts.flattenDiagnosticMessageText(d, "\n");
// File-less (global/options) diagnostics anchor at the entry with a
@@ -1167,8 +1230,20 @@ function preflight7(load: LoadResult): {
* every flagless compile (depth 0 keeps node_modules JS out of the
* program), and the packages themselves still island — so their checker
* errors never gate a build. */
/* WORKSPACE-LINKED shipped JS is the same story at a different path: the
* package's files realpath OUTSIDE node_modules (so depth-0 exclusion
* never saw them and allowJs+checkJs pulls them straight into the
* program), but they are npm surface all the same — the island executes
* them, the program's author cannot fix them (vercel's @vercel/go ships
* an ncc bundle with dozens of checker errors). Registered up front (the
* pass above), suppressed here unless --npm-static opted them into being
* program modules. */
const islandJsFile = (file: string): boolean =>
isJsSourceFileName(file) &&
npmStaticPackageOfPath(file) === null &&
(isNodeModulesPath(file) || workspacePackageOfPath(file) !== null);
const nodeModulesJsSuppressed = (d: ts.Diagnostic): boolean =>
d.fileName !== undefined && isNodeModulesPath(d.fileName) && isJsSourceFileName(d.fileName);
d.fileName !== undefined && islandJsFile(d.fileName);
/* JSDoc TYPE positions in JS files are documentation Node never reads:
* a name-resolution failure THERE (2304/2552 — prettier's utilities.js
* spells a mapped type over a @template name it never declared) types
@@ -1246,7 +1321,13 @@ function preflight7(load: LoadResult): {
sf.fileName !== ambient &&
!sf.isDeclarationFile &&
!sf.fileName.endsWith(".json") &&
(!isNodeModulesPath(sf.fileName) || npmStaticPackageOfPath(sf.fileName) !== null),
(!isNodeModulesPath(sf.fileName) || npmStaticPackageOfPath(sf.fileName) !== null) &&
// Workspace-linked shipped JS (see islandJsFile): its execution
// home is the island exactly like node_modules JS, so no import
// fences, no module edges, no statement counts from it. Its .ts
// files (a workspace package imported bare AND reached relatively)
// stay program source — only the island-bound JS steps out.
!islandJsFile(sf.fileName),
);
const ambientModules = new Set<string>(SUPPORTED_NODE_MODULES);
@@ -1339,7 +1420,7 @@ function preflight7(load: LoadResult): {
if (stmt.isTypeOnly || erasedTypeOnlyReexport(stmt)) continue;
if (!stmt.moduleSpecifier) continue;
const fromSpec = ts.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
if (!fromSpec.startsWith("./") && !fromSpec.startsWith("../")) {
if (!isRelativeSpecifier(fromSpec)) {
// NAMED re-exports from a SUPPORTED builtin pass (`export { ok }
// from "node:assert"` — prettier's universal/assert facade): the
// statement binds nothing locally and evaluates nothing (builtins
@@ -1389,7 +1470,7 @@ function preflight7(load: LoadResult): {
const specNode: ts.Expression | undefined = stmt.moduleSpecifier;
if (specNode === undefined || !ts.isStringLiteral(specNode)) continue;
const spec = specNode.text;
const isRelative = spec.startsWith("./") || spec.startsWith("../");
const isRelative = isRelativeSpecifier(spec);
const isBare = !isRelative && !ambientModules.has(spec);
// An import edge Node's own resolution refuses BEFORE any module
// evaluates: recorded as a startup-crash candidate (the Node-order
@@ -1524,17 +1605,27 @@ function preflight7(load: LoadResult): {
selfImportTdzFences7(program, sf, clause, diags);
}
if (clause) {
// Default imports of SUPPORTED builtins in JS sources pass: Node's
// default export of a CJS builtin IS the module object, so the
// binding exposes exactly the namespace-import surface and the
// lowering keys the same tables (builtinNamespaceModuleOf's
// default-import twin). TS sources keep the fence — the checker
// itself refuses the spelling without interop flags, and the
// SC1012 wording beats the raw TS1259. The callable module
// objects (assert, events, test) stay allowed everywhere.
// Default imports of SUPPORTED builtins pass where the spelling is
// legal: Node's default export of a CJS builtin IS the module
// object, so the binding exposes exactly the namespace-import
// surface and the lowering keys the same tables
// (builtinNamespaceModuleOf's default-import twin). JS sources
// always (Node never asks for interop flags); TS sources when the
// adopted interop knobs made the checker accept the spelling
// (vercel's `import os from 'os'` under esModuleInterop — the
// program TYPECHECKED, so the form is the project's own legal
// dialect). A TS project without interop flags keeps the fence:
// the SC1012 wording beats the raw TS1259 at the same site. The
// callable module objects (assert, events, test) stay allowed
// everywhere.
const opts = program.getCompilerOptions() as {
esModuleInterop?: boolean;
allowSyntheticDefaultImports?: boolean;
};
const interopOn = opts.esModuleInterop === true || opts.allowSyntheticDefaultImports === true;
const defaultOk =
builtinDefaultImportModule(spec) !== null ||
(isJsSourceFileName(sf.fileName) && canonicalBuiltinModule(spec) !== null);
((isJsSourceFileName(sf.fileName) || interopOn) && canonicalBuiltinModule(spec) !== null);
if (clause.name && !isJson && dep === null && !defaultOk) {
diags.push(unsupportedDiag("SC1012", locOf7(clause.name)));
}
@@ -1627,7 +1718,7 @@ function preflight7(load: LoadResult): {
continue;
}
}
const isRelative = req.spec.startsWith("./") || req.spec.startsWith("../");
const isRelative = isRelativeSpecifier(req.spec);
if (!isRelative) {
if (
canonicalBuiltinModule(req.spec) === null &&
@@ -1671,7 +1762,7 @@ function preflight7(load: LoadResult): {
for (const call of nestedBareRequiresOf7(sf)) {
const spec = requireSpecOf7(call)!;
const loc = { file: sf.fileName, start: call.getStart(sf), end: call.getEnd() };
if (!spec.startsWith("./") && !spec.startsWith("../")) {
if (!isRelativeSpecifier(spec)) {
if (canonicalBuiltinModule(spec) === null && !processModuleAliasRequire7(spec, null)) {
// Binding-less by construction — same require-site throw
// channel as the statement-level form above.
@@ -1914,7 +2005,7 @@ function cjsNamedImportLinkCheck(
diags: ScrDiagnostic[],
): StartupCrash | null {
const lexMemo = new Map<ts.SourceFile, Set<string>>();
const isRelative = (spec: string): boolean => spec.startsWith("./") || spec.startsWith("../");
const isRelative = isRelativeSpecifier;
// Relative specifiers resolve as ever; PROJECT imports (#alias/self-name
// — the same package.json-mediated edges preflight admits) join them so
// a named import THROUGH one of a CommonJS module keeps Node's lexer
@@ -2168,7 +2259,7 @@ export function orderedImportsOf(
if (ts.isImportDeclaration(stmt) && erasedTypeOnlyImport(stmt)) continue;
if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
const spec = stmt.moduleSpecifier.text;
const isRelative = spec.startsWith("./") || spec.startsWith("../");
const isRelative = isRelativeSpecifier(spec);
// Relative edges as ever; PROJECT imports (#alias/self-name — the
// package.json-mediated specifiers preflight admits as user-module
// edges) resolve to the same dep so the importer's %init header calls
@@ -2190,7 +2281,7 @@ export function orderedImportsOf(
* already classified the import; this is the lookup the module-order and
* lowering paths share. */
function npmStaticDepSf7(program: ts.Program, sf: ts.SourceFile, spec: string): ts.SourceFile | null {
if (!npmStaticActive() || spec.startsWith("./") || spec.startsWith("../")) return null;
if (!npmStaticActive() || isRelativeSpecifier(spec)) return null;
if (spec.startsWith("node:") || spec.startsWith("#")) return null;
const npm = resolveNpmImport7(sf.fileName, spec);
if (npm === null || !isNpmStaticPackage(npm.packageName)) return null;
+10
View File
@@ -118,6 +118,16 @@ export function workspacePackageOfPath(path: string): string | null {
return null;
}
/** Node's relative-specifier family: './x', '../x', and the bare '.' /
* '..' directory forms (path resolution treats them identically —
* vercel's CLI imports `from '..'` for a parent directory's index).
* Anything else is a package, builtin, or package.json-mediated
* specifier. ('...' and friends are legal PACKAGE names — only the exact
* dot forms are relative.) */
export function isRelativeSpecifier(spec: string): boolean {
return spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../");
}
/** Package name from a path under node_modules — the LAST node_modules
* segment (nested installs blame the innermost package), scoped-aware:
* ".../node_modules/@scope/pkg/dist/x.d.ts" → "@scope/pkg". Paths with no
+11 -6
View File
@@ -11,7 +11,7 @@ import { checkPreflight, isNodeTypesPath, loadProgram, resolveNpmImport, type Lo
import { npmStaticIneligibleReason, npmStaticOffenders, npmStaticPackageOfPath } from "./frontend/npm-static.js";
import { provenanceSources } from "./frontend/provenance-registry.js";
import { resolveBareModule } from "./frontend/resolve.js";
import { isJsSourceFileName } from "./frontend/shared.js";
import { isJsSourceFileName, isRelativeSpecifier } from "./frontend/shared.js";
import { lowerToIr, type LowerOptions, type LowerResult } from "./frontend/lowering/lowerer.js";
import type { CoverageInput, NpmStaticStatus } from "./coverage/report.js";
@@ -147,21 +147,26 @@ interface Frontend {
* Rejected candidates report their reason so the coverage output says why
* auto skipped them. */
function detectAutoPackages(load: LoadResult, statuses: NpmStaticStatus[]): string[] {
const seen = new Map<string, string>(); // package → typesFile
// package → the resolved types file AND the file whose import found it:
// the runtime-JS probe below must resolve from the SAME importing file,
// or a package visible only to a nested package.json realm (a pnpm
// monorepo's packages/*/node_modules, unreachable from the entry's own
// walk-up) answers "no runtime JS" for perfectly ordinary installs.
const seen = new Map<string, { typesFile: string; fromFile: string }>();
for (const sf of [...load.moduleOrder, load.entry]) {
if (sf.fileName.includes("/node_modules/")) continue;
for (const stmt of sf.statements) {
if (!ts7IsImportWithStringSpec(stmt)) continue;
const spec = (stmt as { moduleSpecifier: { text: string } }).moduleSpecifier.text;
if (spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("node:") || spec.startsWith("#")) continue;
if (isRelativeSpecifier(spec) || spec.startsWith("node:") || spec.startsWith("#")) continue;
const npm = resolveNpmImport(sf.fileName, spec);
if (npm === null || isNodeTypesPath(npm.typesFile)) continue;
if (!seen.has(npm.packageName)) seen.set(npm.packageName, npm.typesFile);
if (!seen.has(npm.packageName)) seen.set(npm.packageName, { typesFile: npm.typesFile, fromFile: sf.fileName });
}
}
const chosen: string[] = [];
for (const [pkg, typesFile] of seen) {
const jsEntry = resolveBareModule(load.entry.fileName, pkg, "js-only");
for (const [pkg, { typesFile, fromFile }] of seen) {
const jsEntry = resolveBareModule(fromFile, pkg, "js-only");
const reason = npmStaticIneligibleReason(
pkg,
typesFile,
@@ -4010,6 +4010,21 @@
],
"diags": []
},
"<repo>/tests/corpus/2390-dot-requires/main.cjs": {
"order": [
"<repo>/tests/corpus/2390-dot-requires/shared.cjs",
"<repo>/tests/corpus/2390-dot-requires/sub/index.cjs",
"<repo>/tests/corpus/2390-dot-requires/sub/child.cjs",
"<repo>/tests/corpus/2390-dot-requires/main.cjs"
],
"diags": []
},
"<repo>/tests/corpus/2391-default-builtin-imports/main.ts": {
"order": [
"<repo>/tests/corpus/2391-default-builtin-imports/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/240-stdout-write.ts": {
"order": [
"<repo>/tests/corpus/240-stdout-write.ts"
@@ -5829,6 +5844,48 @@
],
"diags": []
},
"<repo>/tests/fixtures/node-types/argv-env.ts": {
"order": [
"<repo>/tests/fixtures/node-types/argv-env.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/fenced.ts": {
"order": [
"<repo>/tests/fixtures/node-types/fenced.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/import-fences.ts": {
"order": [
"<repo>/tests/fixtures/node-types/import-fences.ts"
],
"diags": [
{
"code": "SC1010",
"message": "the 'v8' module is not supported yet",
"loc": {
"file": "<repo>/tests/fixtures/node-types/import-fences.ts",
"start": 422,
"end": 461
},
"milestone": "M4",
"hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), installed npm packages (their code runs under --dynamic), and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, readline, events, stream, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, and node:test modules (bare or node:-prefixed) are supported"
}
]
},
"<repo>/tests/fixtures/node-types/path-os.ts": {
"order": [
"<repo>/tests/fixtures/node-types/path-os.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/stream-capture.ts": {
"order": [
"<repo>/tests/fixtures/node-types/stream-capture.ts"
],
"diags": []
},
"<repo>/tests/fixtures/npm/cases/abandoned-handles/main.ts": {
"order": [
"<repo>/tests/fixtures/npm/cases/abandoned-handles/main.ts"
@@ -6100,6 +6157,12 @@
],
"diags": []
},
"<repo>/tests/fixtures/npm/cases/workspace-unclean/main.ts": {
"order": [
"<repo>/tests/fixtures/npm/cases/workspace-unclean/main.ts"
],
"diags": []
},
"<repo>/tests/fixtures/npm/cases/zlib-shims/main.ts": {
"order": [
"<repo>/tests/fixtures/npm/cases/zlib-shims/main.ts"
@@ -6166,48 +6229,6 @@
"hint": "set \"strictNullChecks\": true (or \"strict\": true) in the project tsconfig"
}
]
},
"<repo>/tests/fixtures/node-types/argv-env.ts": {
"order": [
"<repo>/tests/fixtures/node-types/argv-env.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/fenced.ts": {
"order": [
"<repo>/tests/fixtures/node-types/fenced.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/import-fences.ts": {
"order": [
"<repo>/tests/fixtures/node-types/import-fences.ts"
],
"diags": [
{
"code": "SC1010",
"message": "the 'v8' module is not supported yet",
"loc": {
"file": "<repo>/tests/fixtures/node-types/import-fences.ts",
"start": 422,
"end": 461
},
"milestone": "M4",
"hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), installed npm packages (their code runs under --dynamic), and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, readline, events, stream, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, and node:test modules (bare or node:-prefixed) are supported"
}
]
},
"<repo>/tests/fixtures/node-types/path-os.ts": {
"order": [
"<repo>/tests/fixtures/node-types/path-os.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/stream-capture.ts": {
"order": [
"<repo>/tests/fixtures/node-types/stream-capture.ts"
],
"diags": []
}
}
}
@@ -20,6 +20,7 @@ import {
resolveRelativeModule,
resolveTypeDirective,
} from "../../src/frontend/resolve.js";
import { isRelativeSpecifier } from "../../src/frontend/shared.js";
import { options5 } from "./harness.js";
const repoRoot = join(import.meta.dirname, "../../../..");
@@ -96,7 +97,9 @@ test("relative and bare specifiers across the whole test tree resolve identicall
for (const file of files) {
for (const spec of specifiersOf(file)) {
const reference = ts5.resolveModuleName(spec, file, OPTS, ts5.sys).resolvedModule ?? null;
if (spec.startsWith("./") || spec.startsWith("../")) {
// Bare '.' and '..' are the relative directory forms (SEMANTICS 362)
// — ts5 resolves them relative too, so they ride the relative arm.
if (isRelativeSpecifier(spec)) {
relative++;
const ours = resolveRelativeModule(file, spec);
// The ONE deliberate relative-resolution delta: a PROJECT
+7
View File
@@ -0,0 +1,7 @@
// The bare '.' and '..' relative-specifier forms (Node's CJS resolution
// treats them exactly like './' and '../' directory imports: the target
// directory's package.json "main", else its index file) — vercel's CLI
// spells `from '..'` at 41 sites, and the CJS require twin is the form
// Node itself runs, so this case pins both lanes byte-for-byte.
const child = require("./sub/child.cjs");
console.log(child.report());
@@ -0,0 +1,4 @@
{
"type": "commonjs",
"main": "./shared.cjs"
}
@@ -0,0 +1,3 @@
// What `require('..')` answers from inside sub/: this directory's
// package.json "main" target.
module.exports = { tag: "parent-main" };
@@ -0,0 +1,5 @@
const parent = require(".."); // the case directory -> package.json main -> shared.cjs
const self = require("."); // this directory -> sub/package.json main -> index.cjs
module.exports.report = function () {
return parent.tag + ":" + self.name;
};
@@ -0,0 +1 @@
module.exports = { name: "sub-index" };
@@ -0,0 +1,4 @@
{
"type": "commonjs",
"main": "./index.cjs"
}
@@ -0,0 +1,15 @@
// DEFAULT imports of supported builtins in a TS source, under the
// project's own adopted interop knob (esModuleInterop in this case's
// tsconfig): Node's default export of a CJS builtin IS the module object,
// so the binding exposes exactly the namespace-import surface — the same
// tables `import * as fs` keys (vercel's CLI spells `import fs from
// 'node:fs'` and `import path from 'path'` throughout). Without the
// interop knob the SC1012 fence stands; with it, the checker accepted the
// spelling and so does the lowering.
import fs from "node:fs";
import path from "path";
import url from "node:url";
console.log(fs.existsSync(process.cwd()) ? "cwd-exists" : "cwd-missing");
console.log(path.join("a", "b"));
console.log(url.fileURLToPath("file:///tmp/x.txt"));
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true
}
}
@@ -0,0 +1,18 @@
// One member of a benign import cycle whose top level calls a BUILTIN
// with a BUILTIN function value as the argument (vercel's link.ts spells
// `const readFile = promisify(fs.readFile)` at the top of a 5-cycle
// cluster; Object.freeze(JSON.parse) is the same shape on scriptc's own
// declared surface). No user code can run during the init window — the
// callee and the callable argument are both dts-rooted — so the cycle
// admission must hold and the module graph must load (the statement
// itself may still be a statement-level blocker; that is a different
// fence than SC1016).
import { helper } from "./b";
const frozenParse = Object.freeze(JSON.parse);
export const A = "a";
export function go(n: number): string {
return helper(n) + ":" + typeof frozenParse;
}
@@ -0,0 +1,7 @@
// Closes the cycle with a NAMED import read only from a function body
// (the deferred position the admission demands).
import { A } from "./a";
export function helper(n: number): string {
return A + n;
}
@@ -0,0 +1,3 @@
import { go } from "./a";
console.log(go(3));
@@ -0,0 +1,2 @@
// What `from ".."` answers inside lib/: this directory's index module.
export const banner = "parent-banner";
@@ -0,0 +1,11 @@
// The bare '.' and '..' relative-specifier forms in the TS project
// dialect (vercel's CLI: `from '..'` at 41 sites): '..' is the parent
// DIRECTORY's index module, '.' this directory's own — exactly what the
// project's bundler-resolution checker answers, so preflight and the
// lowering resolve the same edges. (Node running raw TS would refuse a
// directory import in ESM; the compiled program is the tsc dialect —
// SEMANTICS.md's relative-specifier note.)
import { banner } from "..";
import { local } from ".";
export const title = banner + ":" + local + "!";
@@ -0,0 +1 @@
export const local = "lib-index";
@@ -0,0 +1,3 @@
import { title } from "./lib/child";
console.log(title);
+5
View File
@@ -0,0 +1,5 @@
import shouty from "shouty";
export function run(): string {
return shouty("quiet words");
}
@@ -0,0 +1 @@
export default function shouty(text: string): string;
+5
View File
@@ -0,0 +1,5 @@
export default function shouty(text) {
let out = "";
for (const ch of text) out += ch === " " ? "_" : ch.toUpperCase();
return out + "!";
}
@@ -0,0 +1,6 @@
{
"name": "shouty",
"version": "1.0.0",
"type": "module",
"exports": "./index.js"
}
+10
View File
@@ -0,0 +1,10 @@
// The auto probe's resolution ANCHOR: shouty is installed only in
// inner/'s own node_modules — visible from the importing file's walk-up,
// invisible from this entry's (the pnpm-monorepo shape: vercel's CLI deps
// live in packages/cli/node_modules while the driver entry sits outside).
// auto must probe the runtime JS from the file that IMPORTS the package,
// or eligibility answers "no runtime JS entry resolves" for a perfectly
// ordinary install.
import { run } from "./inner/app.ts";
console.log(run());
+10
View File
@@ -0,0 +1,10 @@
// A workspace-linked UNTYPED package (main → shipped CJS, no .d.ts, the
// @vercel/go shape): its realpath escapes node_modules, so allowJs pulls
// the shipped JS into the checker's program — and the checker refuses
// shapes Node runs fine. Those errors are the foreign-tsconfig story
// (third-party shipped JS the program's author cannot fix); the package's
// execution home is the island, so they never gate, and this program
// must build AND agree with Node.
import { describe } from "wsunclean";
console.log(describe(21));
+1
View File
@@ -0,0 +1 @@
../../../workspace/wsunclean
+22
View File
@@ -0,0 +1,22 @@
"use strict";
// The bundled-dist diet: shapes tsc's checkJs REFUSES but Node runs
// happily (@vercel/go's ncc bundle carries dozens of each — a function
// binding reassigned, an executor-less-looking resolve(), an empty if
// body). The package ships NO declarations and its realpath lies OUTSIDE
// node_modules (the workspace link), so the checker's program pulls this
// file in and finds the errors — which must never gate: the island is
// this file's one execution home.
function greet(name) {
return "hi " + name;
}
greet = function (name) {
return "hello " + name;
};
if (process.env.WSUNCLEAN_NEVER_SET) ;
var ready = new Promise(function (resolve) {
resolve();
});
exports.describe = function (n) {
return "wsunclean:" + greet("go") + ":" + n * 2;
};
exports.ready = ready;
+5
View File
@@ -0,0 +1,5 @@
{
"name": "wsunclean",
"version": "1.0.0",
"main": "./dist/index"
}
+20
View File
@@ -127,6 +127,26 @@ test("type errors render the same code frame a build prints", () => {
expect(out).toContain("^");
});
test("bare '.' and '..' imports are relative edges (the vercel-CLI dialect)", () => {
// `from '..'` / `from '.'` resolve as directory imports to the target's
// index module — exactly what the project's bundler-resolution checker
// answered — so the graph loads and everything compiles statically.
const out = report(fixture("dot-parent/main.ts"));
expect(out).toContain("(100%)");
expect(out).toContain("fully static");
});
test("a cycle whose top level calls a builtin with a builtin function value is admitted", () => {
// vercel's link.ts cluster: `promisify(fs.readFile)` at a cycle member's
// top level. Callee and callable argument are both dts-rooted — no user
// code can run in the init window — so no SC1016 and the analysis
// renders statement counts (the statement itself may stay a
// statement-LEVEL blocker; that is a different fence).
const out = report(fixture("cycle-inert-builtin/main.ts"));
expect(out).not.toContain("circular imports");
expect(out).toContain("statements analyzed");
});
test("every corpus program is 100% static (corpus and coverage agree)", async () => {
// The differential corpus compiles by definition; coverage must agree.
// `// @dynamic` programs compile under --dynamic, so analyze them that way.
+23 -3
View File
@@ -50,15 +50,15 @@ async function runBinary(cmd: string, args: string[]): Promise<RunResult> {
/** Compile one pilot statically (no --dynamic — the whole point) with the
* named packages opted in; cache-keyed over the program and the vendored
* packages. */
async function buildStatic(entry: string, npmStatic: string[]): Promise<string> {
async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise<string> {
const hash = createHash("sha256");
const inputs = [
entry,
...globSync(join(pilotRoot, "node_modules/**/*.{js,mjs,cjs,json,d.ts}")).sort(),
...globSync(join(pilotRoot, "**/node_modules/**/*.{js,mjs,cjs,json,d.ts}")).sort(),
];
for (const f of inputs) hash.update(f).update(readFileSync(f));
const key = hash
.update(npmStatic.join(","))
.update(npmStatic === "auto" ? "auto" : npmStatic.join(","))
.update(sanitize ? "san" : "plain")
.digest("hex")
.slice(0, 16);
@@ -119,6 +119,26 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
expect(coverage.stats.statementsFailed).toBe(0);
}, 120_000);
// Auto's runtime-JS probe anchors at the IMPORTING file, not the entry:
// shouty is installed only in inner/'s node_modules (the pnpm-monorepo
// shape — vercel's CLI deps live in packages/cli/node_modules while the
// analysis driver sits outside every package realm), so an entry-anchored
// probe answers "no runtime JS entry resolves" for an ordinary install.
test("--npm-static=auto probes runtime JS from the importing file's realm", async () => {
const entry = join(pilotRoot, "nested/main.ts");
const { coverage } = analyze(entry, { npmStatic: "auto" });
expect(coverage.npmStatic).toEqual([{ package: "shouty", status: "static" }]);
expect(coverage.preflightFailed).toBe(false);
expect(coverage.stats.statementsFailed).toBe(0);
const binary = await buildStatic(entry, "auto");
const [nodeRes, nativeRes] = await Promise.all([
runBinary("node", [entry]),
runBinary(binary, []),
]);
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
}, 120_000);
// Auto refuses ms: it ships no own .d.ts (the declared-claim criterion),
// so the import keeps today's story — and the explicit opt-in below is
// the user's override.
+17
View File
@@ -194,3 +194,20 @@ test("no-types-node: a bare project outside the repo compiles on the fallback de
const { stdout } = await execFileAsync(result.binaryPath);
expect(stdout).toBe("hi bare\n");
});
test("dot-parent: bare '.' and '..' imports build and run (the TS project dialect)", async () => {
// The vercel-CLI spelling (`from '..'` for a parent directory's index):
// scriptc-only — Node refuses directory imports when running raw TS as
// ESM, but the compiled program follows the project's own bundler-
// resolution dialect (the SEMANTICS.md relative-specifier note).
const outDir = outDirFor("dot-parent");
const result = await compile(join(repoRoot, "tests/coverage-fixtures/dot-parent/main.ts"), {
outPath: join(outDir, "main"),
outDir,
sanitize,
});
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
if (!result.ok) return;
const { stdout } = await execFileAsync(result.binaryPath);
expect(stdout).toBe("parent-banner:lib-index!\n");
});