mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
- The full legacy surface: parse, stringify, escape, unescape, the aliases - Node's scan state machine in C: separators, maxKeys, malformed-escape and truncation quirks - Link-gated; escape-only programs never link the unit
278 lines
14 KiB
TypeScript
278 lines
14 KiB
TypeScript
/* World-neutral program-lifecycle constants and helpers — strings,
|
|
* numbers, and path predicates only, no AST, checker, or enum objects.
|
|
* Split out when two frontend lanes (typescript@5.9.3 and the TS7
|
|
* adapter) had to share them without exchanging world-typed objects;
|
|
* program.ts re-exports all of it, so lowering-side import sites kept
|
|
* their spelling across the phase-4 flip that retired the 5.9.3 lane. */
|
|
|
|
import { createRequire } from "node:module";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
/** Path of the shipped ambient declarations — the always-shipped CORE
|
|
* (comptime/__island_eval, setTimeout). Part of EVERY program scriptc
|
|
* builds, the project-world preflight program included. */
|
|
export function ambientDtsPath(): string {
|
|
return require.resolve("@scriptc/compiler/scriptc.d.ts");
|
|
}
|
|
|
|
/** Path of the shipped divergence/precision OVERRIDES (JSON.parse():
|
|
* unknown, pop(): T, the Promise executor shape, ...). Part of the LOWERING
|
|
* program only — preflight's project-world second chance builds without it,
|
|
* so a project that typechecks under its own tsc never fails preflight over
|
|
* an override-manufactured error (checkPreflight). */
|
|
export function overridesDtsPath(): string {
|
|
return require.resolve("@scriptc/compiler/scriptc-overrides.d.ts");
|
|
}
|
|
|
|
/** Path of the shipped FALLBACK declarations (console, process, node:fs) —
|
|
* part of the program only when the target project has no @types/node.
|
|
* With @types/node, the project's real Node types stand in and this file
|
|
* stands down (its declaration forms would collide). */
|
|
export function fallbackDtsPath(): string {
|
|
return require.resolve("@scriptc/compiler/scriptc-node-fallback.d.ts");
|
|
}
|
|
|
|
/** True for files belonging to the adopted Node type surface: the
|
|
* @types/node package itself and undici-types (its dependency — the
|
|
* web-platform globals: fetch/Response/AbortSignal/ReadableStream/...).
|
|
* The provenance half of the lowering tables' recognition when the
|
|
* fallback declarations stand down, and of the SC2020-family fence for
|
|
* everything else those packages declare. */
|
|
export function isNodeTypesPath(file: string): boolean {
|
|
const pkg = npmPackageNameOf(file);
|
|
return pkg === "@types/node" || pkg === "undici-types";
|
|
}
|
|
|
|
/** The node builtin modules with scriptc lowerings, by CANONICAL (bare)
|
|
* name — every module answers to both specifier forms ("fs" and "node:fs"
|
|
* are the same module, like in Node). When the fallback declarations ship,
|
|
* this is exactly the set of `declare module` names in that file; when
|
|
* @types/node stands in (which declares ALL node builtins) the supported
|
|
* surface must not widen, so preflight allowlists this same fixed set. */
|
|
export const SUPPORTED_BUILTIN_MODULES = ["fs", "path", "path/posix", "path/win32", "os", "url", "fs/promises", "crypto", "zlib", "child_process", "net", "http", "tls", "https", "dgram", "dns", "util", "util/types", "string_decoder", "querystring", "readline", "http2", "assert", "assert/strict", "worker_threads", "buffer", "cluster", "tty", "async_hooks", "events", "stream", "test", "timers", "timers/promises", "diagnostics_channel", "perf_hooks", "module"] as const;
|
|
|
|
/** Builtins Node itself serves ONLY under the node: prefix —
|
|
* require("test") is MODULE_NOT_FOUND in Node, so the bare name stays a
|
|
* user-package specifier and never canonicalizes to the builtin. "module"
|
|
* joins by CAUTION, not Node's rule (Node serves bare "module"): an npm
|
|
* package named "module" exists, and the supported surface here is only
|
|
* the admit-then-fence-per-member story (createRequire's runtime fence),
|
|
* so the bare spelling keeps resolving as a package name. */
|
|
const PREFIX_ONLY_BUILTIN_MODULES: ReadonlySet<string> = new Set(["test", "module"]);
|
|
|
|
/** The builtin modules whose DEFAULT import binding lowers: node:assert's
|
|
* module object IS a callable function (`import assert from "node:assert";
|
|
* assert(x)`), and node:events' module object IS the EventEmitter class
|
|
* (`module.exports = EventEmitter` — `import EventEmitter from
|
|
* "node:events"; new EventEmitter()`), so those default bindings lower —
|
|
* for every other builtin the default-import fence stands. */
|
|
export function builtinDefaultImportModule(spec: string): string | null {
|
|
const canon = canonicalBuiltinModule(spec);
|
|
// node:test's module object IS the test function (`import test from
|
|
// "node:test"; test(...)`), like assert's callable module object.
|
|
return canon === "assert" || canon === "assert/strict" || canon === "events" || canon === "test"
|
|
? canon
|
|
: null;
|
|
}
|
|
|
|
/** Canonical (bare) name of a supported builtin-module specifier — "fs"
|
|
* for both "fs" and "node:fs" — or null for everything else. The lowering
|
|
* tables and the preflight allowlist share this one normalization. */
|
|
export function canonicalBuiltinModule(spec: string): string | null {
|
|
const bare = spec.startsWith("node:") ? spec.slice(5) : spec;
|
|
if (bare === spec && PREFIX_ONLY_BUILTIN_MODULES.has(bare)) return null;
|
|
return (SUPPORTED_BUILTIN_MODULES as readonly string[]).includes(bare) ? bare : null;
|
|
}
|
|
|
|
/** Both specifier spellings of every supported builtin (prefix-only ones
|
|
* keep just the node: form) — the ambient-module allowlist preflight uses. */
|
|
export const SUPPORTED_NODE_MODULES: readonly string[] = SUPPORTED_BUILTIN_MODULES.flatMap((m) =>
|
|
PREFIX_ONLY_BUILTIN_MODULES.has(m) ? [`node:${m}`] : [m, `node:${m}`],
|
|
);
|
|
|
|
/* ── workspace-linked packages ───────────────────────────────────────────
|
|
* A bare specifier can resolve through node_modules to a SYMLINK whose real
|
|
* location lies outside every node_modules directory — the workspace link
|
|
* every monorepo tool installs for internal packages (pnpm, npm, yarn, and
|
|
* bun workspaces all do). Node resolves and runs such a package exactly
|
|
* like any installed one, and so does scriptc — but the resolved files'
|
|
* REALPATHS carry no node_modules segment, so every path-keyed package
|
|
* attribution (which package declared this symbol, which package does this
|
|
* diagnostic belong to, is this file inside the --npm-static opt-in) needs
|
|
* this registry: real package directory → package name, filled by the
|
|
* resolver as workspace links are discovered and reset per load. */
|
|
|
|
const workspacePackageDirs = new Map<string, string>();
|
|
|
|
export function registerWorkspacePackage(name: string, realDir: string): void {
|
|
workspacePackageDirs.set(realDir.split("\\").join("/"), name);
|
|
}
|
|
|
|
export function clearWorkspacePackages(): void {
|
|
workspacePackageDirs.clear();
|
|
}
|
|
|
|
/** True when `name` is a registered workspace package — the NAME-keyed
|
|
* twin of workspacePackageOfPath, for call sites that hold a bare import
|
|
* specifier instead of a file path (a workspace member installed by COPY
|
|
* has no out-of-node_modules files to match, but its name registered all
|
|
* the same). */
|
|
export function isWorkspacePackageName(name: string): boolean {
|
|
for (const n of workspacePackageDirs.values()) {
|
|
if (n === name) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** The registered workspace package a path lies inside, or null. */
|
|
export function workspacePackageOfPath(path: string): string | null {
|
|
const norm = path.split("\\").join("/");
|
|
for (const [dir, name] of workspacePackageDirs) {
|
|
if (norm === dir || (norm.startsWith(dir) && norm[dir.length] === "/")) return name;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Node's relative-specifier family: './x', '../x', and the bare '.' /
|
|
* '..' directory forms (path resolution treats them identically —
|
|
* real CLIs import `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
|
|
* node_modules segment answer their registered workspace package (the
|
|
* realpath'd home of a symlinked workspace install), else null. */
|
|
export function npmPackageNameOf(file: string): string | null {
|
|
const parts = file.split("/");
|
|
const i = parts.lastIndexOf("node_modules");
|
|
if (i < 0 || i + 1 >= parts.length) return workspacePackageOfPath(file);
|
|
const first = parts[i + 1]!;
|
|
if (first.startsWith("@")) {
|
|
const second = parts[i + 2];
|
|
return second ? `${first}/${second}` : first;
|
|
}
|
|
return first;
|
|
}
|
|
|
|
/* The compiler-options split (documented in SEMANTICS.md; the enum-valued
|
|
* FORCED knobs are spelled per world — each lane owns its enum objects):
|
|
* ADOPTED knobs change which programs typecheck; FORCED knobs change
|
|
* semantics scriptc depends on; strictNullChecks is a FLOOR. */
|
|
export const ADOPTED_OPTIONS = [
|
|
"strict",
|
|
"noImplicitAny",
|
|
"strictNullChecks",
|
|
"strictFunctionTypes",
|
|
"strictBindCallApply",
|
|
"strictPropertyInitialization",
|
|
"strictBuiltinIteratorReturn",
|
|
"noImplicitThis",
|
|
"alwaysStrict",
|
|
"useUnknownInCatchVariables",
|
|
"noUncheckedIndexedAccess",
|
|
"exactOptionalPropertyTypes",
|
|
"noImplicitOverride",
|
|
"noPropertyAccessFromIndexSignature",
|
|
"noImplicitReturns",
|
|
"noFallthroughCasesInSwitch",
|
|
"noUnusedLocals",
|
|
"noUnusedParameters",
|
|
"allowUnusedLabels",
|
|
"allowUnreachableCode",
|
|
"skipLibCheck",
|
|
// Import-form interop knobs: they only change which import FORMS
|
|
// typecheck (default imports of CJS-shaped types) — the import fence
|
|
// rejects unsupported forms with SC-diagnostics either way, and a
|
|
// SC1012 "not supported yet" beats a raw TS1259 at the same site.
|
|
"esModuleInterop",
|
|
"allowSyntheticDefaultImports",
|
|
] as const;
|
|
|
|
/* The JAVASCRIPT strictness stance (the JS-input design made real):
|
|
* JavaScript sources carry no annotations, so tsc's strictness families
|
|
* that exist to demand annotations — or to reject dynamically-typed
|
|
* idioms that are ordinary, working JS — cannot be satisfied by the
|
|
* program's author at all. The design already says where inference gaps
|
|
* land: exactly where `any` lands in TS (island under --dynamic, honest
|
|
* fences without). So for JS FILES ONLY, these tsc families are not
|
|
* preflight gates; the values involved stay `any`/loose and every use
|
|
* meets the lowerer's per-site fences instead. TypeScript sources keep
|
|
* the full strict gate — nothing here weakens the default for annotated
|
|
* programs. */
|
|
export const JS_RELAXED_TSC_CODES: ReadonlySet<number> = new Set([
|
|
// implicit any (params, variables, elements, this, returns, rest,
|
|
// construct signatures, index reads over shapes with no index
|
|
// signature — typeof globalThis)
|
|
2683, 7005, 7006, 7008, 7009, 7010, 7015, 7017, 7019, 7022, 7023, 7024, 7031, 7034, 7053,
|
|
// strict-null narrowing demands (possibly null/undefined receivers)
|
|
2531, 2532, 2533, 18047, 18048, 18049,
|
|
// assignability/arity/callability of dynamic call shapes (valid JS,
|
|
// checked at runtime by Node; the lowerer re-checks every lowered form
|
|
// per site) and member/existence probes over dynamic shapes (expando
|
|
// properties, capability probes — the lowerer fences unknown members
|
|
// per site with the SC2020 family)
|
|
2322, 2339, 2345, 2349, 2351, 2367, 2554, 2555, 2556, 2769,
|
|
2740, // 2322's elaborated "missing the following properties" form
|
|
2305, 2551, 2724, 2731,
|
|
// excess object-literal properties against a JSDoc-inferred contextual
|
|
// type (and the did-you-mean variant): ordinary, working JS — the extra
|
|
// key rides the object at runtime exactly as written (the formatter idiom's
|
|
// expand-patterns passes an ignoreUnknown member its own JSDoc type
|
|
// never declared)
|
|
2353, 2561,
|
|
// JSDoc TYPE-SPACE claims that fail to check: a value name in a type
|
|
// position (2749/2702), generic constraints and index types spelled in
|
|
// typedefs (2344/2536/2538), an async @returns that is not Promise
|
|
// (1064), and 2366's no-ending-return against a JSDoc-declared return
|
|
// type. The claims are documentation — Node never reads them; the
|
|
// values keep their runtime behavior and every use meets the per-site
|
|
// fences (the pattern's source carries dozens of each).
|
|
2749, 2702, 2344, 2536, 2538, 1064, 2366,
|
|
// strict-null demands on dynamic shapes, continued: iterating/invoking
|
|
// possibly-undefined values (runtime re-checks at the site)
|
|
2488, 2721, 2722,
|
|
// an UNUSED @ts-expect-error: the directive annotated an error visible
|
|
// under the project's OWN tsconfig world — scriptc's world deliberately
|
|
// differs (adopted options, hidden declaration twins), and an unused
|
|
// suppression changes nothing at runtime
|
|
2578,
|
|
// a lib-floor demand (es2024+ members under the es2023 lib): the member
|
|
// types as the error-any and its use fences per site with the honest
|
|
// no-lowering story instead of gating the whole program
|
|
2550,
|
|
// `export *` ambiguity (2308): Node DEFINES this case — ambiguous names
|
|
// are excluded from the star surface — where tsc asks for an explicit
|
|
// re-export; and 2305's did-you-mean sibling 2614 joins 2305
|
|
2308, 2614,
|
|
// 2740's sibling elaboration ("Property X is missing in type Y")
|
|
2741,
|
|
// unresolvable modules: Node throws WHEN the require executes — the
|
|
// binding types as any and reached uses fence (never a silent pass)
|
|
2307, 2792,
|
|
// 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 —
|
|
// exactly the implicit-any story above. The values stay 'unknown' and
|
|
// every use meets the lowerer's per-site unknown fences (member reads
|
|
// ride the checked-dynamic DOM; unsupported operations fence loudly).
|
|
18046, 2571,
|
|
]);
|
|
|
|
/* Arithmetic on an `any` operand (TS2362/2363/2365 fire when the other
|
|
* side is bigint-ish): plain JS — relaxed only when 'any' participates. */
|
|
export const JS_ANY_OPERATOR_CODES: ReadonlySet<number> = new Set([2362, 2363, 2365]);
|
|
|
|
/** True for JavaScript source file NAMES (.js/.mjs/.cjs/.jsx) — the files
|
|
* whose types come entirely from inference (checkJs + JSDoc) and whose JS
|
|
* relaxation stance applies. */
|
|
export function isJsSourceFileName(fileName: string): boolean {
|
|
return /\.(js|mjs|cjs|jsx)$/.test(fileName);
|
|
}
|