mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
fix(compiler): support implicit top-level await modules (#94)
* fix(compiler): support implicit top-level await modules - Match Node 24 syntax detection by treating top-level await as an ES module marker. - Add differential corpus coverage for TypeScript files without explicit imports or exports. * fix Node ESM classification for top-level await * fix ESM require diagnostics and classification caching * fix require binding and package type precedence * fix: align top-level await module detection with Node
This commit is contained in:
@@ -334,6 +334,34 @@ export function tscPassthroughDiag(message: string, loc: SrcLoc): ScrDiagnostic
|
||||
return { code: "SC0001", message, loc };
|
||||
}
|
||||
|
||||
/** Node's package scope forces CommonJS, but the source contains a marker
|
||||
* that is legal only when the file is parsed as ESM (static import/export,
|
||||
* import.meta, top-level await, or a lexical CommonJS-wrapper redeclaration).
|
||||
* TypeScript's bundler-mode checker does not model this Node loader rule. */
|
||||
export function commonJsModuleSyntaxDiag(loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC1013",
|
||||
message: "Node classifies this file as CommonJS, where this ES-module syntax marker is a SyntaxError",
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "use a .mjs/.mts extension or set the nearest package.json's \"type\" to \"module\"",
|
||||
};
|
||||
}
|
||||
|
||||
/** Node stops package-scope lookup at the nearest package.json even when it
|
||||
* is malformed, then refuses the module with ERR_INVALID_PACKAGE_CONFIG.
|
||||
* The compiler must fail before lowering instead of inheriting a parent
|
||||
* package's type and producing a runnable binary. */
|
||||
export function invalidPackageConfigDiag(packageJsonPath: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC1013",
|
||||
message: `Node cannot load this module because '${packageJsonPath}' is an invalid package configuration (ERR_INVALID_PACKAGE_CONFIG)`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "fix the nearest package.json so it contains valid JSON",
|
||||
};
|
||||
}
|
||||
|
||||
/** The project's tsconfig disables strictNullChecks, which scriptc cannot
|
||||
* adopt: null and undefined are DISTINCT STATIC TYPES in the value model
|
||||
* (union arms with their own runtime representation) — with the knob off,
|
||||
|
||||
@@ -4114,7 +4114,7 @@ function isEsModuleStamp(expr: ts.Expression): boolean {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isJsSourceFile(sf) && !ts.isExternalModule(sf)) {
|
||||
if (isCjsJsFile(sf)) {
|
||||
const init = L.requireInitStmt(spec, expr);
|
||||
if (init) return init;
|
||||
return { kind: "block", body: [], loc: locOf(expr) };
|
||||
|
||||
@@ -28,11 +28,10 @@
|
||||
* in tsc codes at all: cjsExportDiscardReason (lowering) names every
|
||||
* genuinely discarded export, so relaxing the checker's stricter
|
||||
* modeling loses nothing.
|
||||
* - isNodeEsmFile: 5.9.3 under bundler sets impliedNodeFormat ONLY for
|
||||
* .mjs/.cjs-family files (probed) and falls back to syntactic module
|
||||
* detection for .js/.ts; 7's client SourceFile has no impliedNodeFormat,
|
||||
* so this lane spells exactly that observed behavior: extensions first,
|
||||
* then externalModuleIndicator.
|
||||
* - isNodeEsmFile: 7's client SourceFile has no impliedNodeFormat, so this
|
||||
* lane spells Node's format decision directly: fixed extensions first,
|
||||
* then an explicit nearest-package `type`, then syntax detection for
|
||||
* ambiguous .js/.ts files.
|
||||
* - Malformed tsconfig.json: tsgo's parseConfigFile swallows JSONC syntax
|
||||
* errors (recovers to empty options) where 5.9.3 reported a passthrough
|
||||
* diagnostic, so this lane validates the config's JSONC syntax itself
|
||||
@@ -46,11 +45,13 @@ import { dirname, resolve } from "node:path";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import {
|
||||
commonJsModuleSyntaxDiag,
|
||||
invalidPackageConfigDiag,
|
||||
strictNullChecksFloorDiag,
|
||||
tscPassthroughDiag,
|
||||
unsupportedDiag,
|
||||
} from "../diagnostics/diagnostic.js";
|
||||
import { isNodeModulesPath, nearestPkgJsonPath, projectDtsRuntimeSibling, resolveBareModule, resolveProjectImport, resolveRelativeModule, resolveTypeDirective, setProjectRealm } from "./resolve.js";
|
||||
import { isNodeModulesPath, nearestInvalidPackageJsonPath, nearestPackageType, nearestPkgJsonPath, projectDtsRuntimeSibling, resolveBareModule, resolveProjectImport, resolveRelativeModule, resolveTypeDirective, setProjectRealm } from "./resolve.js";
|
||||
import { probeNodeImportRefusal, probeNodeRequireRefusal } from "./npm.js";
|
||||
import { isNpmStaticPackage, npmStaticActive, npmStaticFsShadow, npmStaticPackageOfPath, reportNpmStaticOffender, setNpmStaticPackages } from "./npm-static.js";
|
||||
import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js";
|
||||
@@ -84,6 +85,14 @@ const FORCED_OPTIONS: ts.Ts7CompilerOptions = {
|
||||
target: ts.ScriptTarget.ESNext as number,
|
||||
module: ts.ModuleKind.ESNext as number,
|
||||
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
||||
// Every file Node executes has its own lexical module wrapper: ESM has a
|
||||
// module environment and CommonJS has the per-file function wrapper. In
|
||||
// particular, an ambiguous file whose ONLY ESM marker is top-level await
|
||||
// must not remain a TypeScript global script (otherwise declarations in
|
||||
// sibling files merge and reads cross module boundaries). Runtime format
|
||||
// classification is intentionally independent below; this option is only
|
||||
// the binder's scope decision.
|
||||
moduleDetection: ts.ModuleDetectionKind.Force,
|
||||
lib: ["lib.es2025.d.ts"],
|
||||
types: [],
|
||||
allowImportingTsExtensions: true,
|
||||
@@ -646,15 +655,213 @@ function nestedBareRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
type RequireCallBindingKind7 = "ambient" | "createRequire" | "source";
|
||||
|
||||
function stripRequireCasts7(expr: ts.Expression): ts.Expression {
|
||||
let cur = expr;
|
||||
while (
|
||||
ts.isParenthesizedExpression(cur) ||
|
||||
ts.isAsExpression(cur) ||
|
||||
ts.isTypeAssertion(cur) ||
|
||||
ts.isNonNullExpression(cur)
|
||||
) {
|
||||
cur = cur.expression;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
/** The supported-builtin provenance of an imported identifier, narrowed to
|
||||
* the one member this preflight distinction needs. Mirrors the lowering's
|
||||
* builtinImportOf alias handling so a re-export facade remains supported. */
|
||||
function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): boolean {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(ident);
|
||||
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
|
||||
if (decl === undefined || !ts.isImportSpecifier(decl)) return false;
|
||||
const importDecl = decl.parent.parent.parent;
|
||||
if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) {
|
||||
const module = canonicalBuiltinModule(importDecl.moduleSpecifier.text);
|
||||
const member = decl.propertyName?.text ?? decl.name.text;
|
||||
if (module === "module" && member === "createRequire") return true;
|
||||
}
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
||||
const target = checker.getAliasedSymbol(symbol);
|
||||
const targetDecl = checker.declarationsOf(target)[0];
|
||||
if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) {
|
||||
for (let p: ts.Node | undefined = targetDecl.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
|
||||
if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) {
|
||||
return canonicalBuiltinModule(p.name.text) === "module" && target.name === "createRequire";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True only for the createRequire binding shape the lowering recognizes:
|
||||
* a const initialized from node:module's createRequire with a base naming
|
||||
* the current file. Arbitrary source declarations named `require` must not
|
||||
* ride the module-edge lowering, which would erase their calls. */
|
||||
function isCreateRequireBinding7(program: ts.Program, callee: ts.Identifier): boolean {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(callee);
|
||||
const decl = symbol
|
||||
? checker.declarationsOf(symbol).find((d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d))
|
||||
: undefined;
|
||||
if (
|
||||
decl === undefined ||
|
||||
decl.initializer === undefined ||
|
||||
!ts.isVariableDeclarationList(decl.parent) ||
|
||||
(decl.parent.flags & ts.NodeFlags.Const) === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const init = stripRequireCasts7(decl.initializer);
|
||||
if (
|
||||
!ts.isCallExpression(init) ||
|
||||
init.questionDotToken !== undefined ||
|
||||
!ts.isIdentifier(init.expression) ||
|
||||
!isCreateRequireImport7(program, init.expression) ||
|
||||
init.arguments.length !== 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const base = stripRequireCasts7(init.arguments[0]!);
|
||||
if (ts.isIdentifier(base) && base.text === "__filename") return true;
|
||||
return (
|
||||
ts.isPropertyAccessExpression(base) &&
|
||||
base.questionDotToken === undefined &&
|
||||
ts.isMetaProperty(base.expression) &&
|
||||
base.expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
|
||||
(base.name.text === "url" || base.name.text === "filename")
|
||||
);
|
||||
}
|
||||
|
||||
/** Classifies a require-shaped call as Node's ambient CommonJS global, the
|
||||
* one supported createRequire source binding, or another source binding.
|
||||
* The scanners still collect createRequire calls because they carry static
|
||||
* module edges; other source bindings get a pointed fence before lowering
|
||||
* can mistake them for module syntax. */
|
||||
function requireCallBindingKind7(program: ts.Program, node: ts.Node): RequireCallBindingKind7 {
|
||||
const call = ts.isVariableDeclaration(node) ? node.initializer : node;
|
||||
if (!call || !ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) return "ambient";
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(call.expression);
|
||||
if (symbol === undefined) return "ambient";
|
||||
const decls = checker.declarationsOf(symbol);
|
||||
if (decls.length === 0 || decls.every((d) => d.getSourceFile().isDeclarationFile)) return "ambient";
|
||||
return isCreateRequireBinding7(program, call.expression) ? "createRequire" : "source";
|
||||
}
|
||||
|
||||
function esmRequireFeature7(kind: Exclude<RequireCallBindingKind7, "createRequire">): string {
|
||||
return kind === "ambient"
|
||||
? "require() in an ES module (Node throws ReferenceError — use import)"
|
||||
: "calls through a source binding named 'require' (this spelling is reserved for CommonJS/createRequire module edges — rename the binding)";
|
||||
}
|
||||
|
||||
const CJS_WRAPPER_NAMES = new Set(["require", "module", "exports", "__dirname", "__filename"]);
|
||||
|
||||
function bindingDeclaresCjsWrapper7(name: ts.BindingName): ts.Identifier | null {
|
||||
if (ts.isIdentifier(name)) return CJS_WRAPPER_NAMES.has(name.text) ? name : null;
|
||||
for (const el of name.elements) {
|
||||
if (el.name === undefined) continue;
|
||||
const hit = bindingDeclaresCjsWrapper7(el.name);
|
||||
if (hit !== null) return hit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function insideFunctionLike7(node: ts.Node): boolean {
|
||||
for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
|
||||
if (ts.isFunctionLike(p)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The first syntax marker Node's ambiguous-file detector recognizes. This
|
||||
* cannot use ts.isExternalModule: moduleDetection=force deliberately gives
|
||||
* EVERY executable file module-local checker scope, including CommonJS.
|
||||
* Runtime classification remains Node's own narrower question: value-level
|
||||
* import/export forms, import.meta, top-level await/for-await, or a top-level
|
||||
* lexical declaration that would redeclare a CommonJS wrapper binding. */
|
||||
function nodeEsmSyntaxMarker7(sf: ts.SourceFile): ts.Node | null {
|
||||
for (const stmt of sf.statements) {
|
||||
if (ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) return stmt;
|
||||
if (
|
||||
ts.isExportDeclaration(stmt) &&
|
||||
!stmt.isTypeOnly &&
|
||||
!erasedTypeOnlyReexport(stmt)
|
||||
) {
|
||||
return stmt;
|
||||
}
|
||||
if (ts.isExportAssignment(stmt)) return stmt;
|
||||
if (
|
||||
ts.canHaveModifiers(stmt) &&
|
||||
ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) === true
|
||||
) {
|
||||
return stmt;
|
||||
}
|
||||
if (
|
||||
ts.isVariableStatement(stmt) &&
|
||||
(stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0
|
||||
) {
|
||||
for (const decl of stmt.declarationList.declarations) {
|
||||
const hit = bindingDeclaresCjsWrapper7(decl.name);
|
||||
if (hit !== null) return hit;
|
||||
}
|
||||
}
|
||||
if (ts.isClassDeclaration(stmt) && stmt.name !== undefined && CJS_WRAPPER_NAMES.has(stmt.name.text)) {
|
||||
return stmt.name;
|
||||
}
|
||||
}
|
||||
|
||||
let found: ts.Node | null = null;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
ts.isMetaProperty(node) &&
|
||||
node.keywordToken === ts.SyntaxKind.ImportKeyword
|
||||
) {
|
||||
found = node;
|
||||
return "stop";
|
||||
}
|
||||
if (
|
||||
!insideFunctionLike7(node) &&
|
||||
(ts.isAwaitExpression(node) ||
|
||||
(ts.isForOfStatement(node) && node.awaitModifier !== undefined))
|
||||
) {
|
||||
found = node;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
const nodeEsmFileCache7 = new WeakMap<ts.SourceFile, boolean>();
|
||||
|
||||
/** True when Node would treat this file as an ES MODULE (never defining
|
||||
* require/__dirname there): .mjs always, .cjs never, and .js/.ts by
|
||||
* syntactic module detection — spelled to 5.9.3's OBSERVED bundler-mode
|
||||
* behavior (it set impliedNodeFormat only for the mjs/cjs families; 7's
|
||||
* client SourceFile has no impliedNodeFormat at all). */
|
||||
* require/__dirname there): fixed extensions first, then an explicit
|
||||
* nearest-package type, then Node's syntax markers for ambiguous .js/.ts
|
||||
* files. TypeScript's external-module bit is forced for checker scoping and
|
||||
* therefore intentionally plays no part in this runtime decision. */
|
||||
function isNodeEsmFile7(sf: ts.SourceFile): boolean {
|
||||
if (sf.fileName.endsWith(".cjs") || sf.fileName.endsWith(".cts")) return false;
|
||||
if (sf.fileName.endsWith(".mjs") || sf.fileName.endsWith(".mts")) return true;
|
||||
return ts.isExternalModule(sf);
|
||||
const cached = nodeEsmFileCache7.get(sf);
|
||||
if (cached !== undefined) return cached;
|
||||
let result: boolean;
|
||||
if (sf.fileName.endsWith(".cjs") || sf.fileName.endsWith(".cts")) {
|
||||
result = false;
|
||||
} else if (sf.fileName.endsWith(".mjs") || sf.fileName.endsWith(".mts")) {
|
||||
result = true;
|
||||
} else {
|
||||
const packageType = nearestPackageType(sf.fileName);
|
||||
result = packageType === "module"
|
||||
? true
|
||||
: packageType === "commonjs"
|
||||
? false
|
||||
: nodeEsmSyntaxMarker7(sf) !== null;
|
||||
}
|
||||
nodeEsmFileCache7.set(sf, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A JavaScript source file Node treats as CommonJS (not an ES module):
|
||||
@@ -1611,6 +1818,28 @@ function preflight7(load: LoadResult): {
|
||||
!islandJsFile(sf.fileName),
|
||||
);
|
||||
|
||||
// Node stops at the nearest package.json even when it is malformed, and
|
||||
// an explicit CommonJS scope (or .cjs/.cts extension) disables ambiguous-
|
||||
// file syntax detection entirely. TypeScript's bundler checker models
|
||||
// neither loader rule, so restore them before any module-edge analysis:
|
||||
// one malformed-scope diagnostic per package file, and one pointed syntax
|
||||
// diagnostic per CommonJS source file.
|
||||
const invalidPackageConfigs = new Set<string>();
|
||||
for (const sf of userFiles) {
|
||||
const invalidConfig = nearestInvalidPackageJsonPath(sf.fileName);
|
||||
if (invalidConfig !== null) {
|
||||
if (!invalidPackageConfigs.has(invalidConfig)) {
|
||||
invalidPackageConfigs.add(invalidConfig);
|
||||
diags.push(invalidPackageConfigDiag(invalidConfig, { file: sf.fileName, start: 0, end: 0 }));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!isNodeEsmFile7(sf)) {
|
||||
const marker = nodeEsmSyntaxMarker7(sf);
|
||||
if (marker !== null) diags.push(commonJsModuleSyntaxDiag(locOf7(marker)));
|
||||
}
|
||||
}
|
||||
|
||||
const ambientModules = new Set<string>(SUPPORTED_NODE_MODULES);
|
||||
|
||||
// Ambient `declare module "name"` declarations anywhere in the program —
|
||||
@@ -2008,10 +2237,11 @@ function preflight7(load: LoadResult): {
|
||||
for (const req of requiresOf7(stmt)) {
|
||||
const loc = { file: sf.fileName, start: req.node.getStart(sf), end: req.node.getEnd() };
|
||||
if (isNodeEsmFile7(sf)) {
|
||||
diags.push(
|
||||
unsupportedDiag("SC1013", loc, "require() in an ES module (Node throws ReferenceError — use import)"),
|
||||
);
|
||||
continue;
|
||||
const bindingKind = requireCallBindingKind7(program, req.node);
|
||||
if (bindingKind !== "createRequire") {
|
||||
diags.push(unsupportedDiag("SC1013", loc, esmRequireFeature7(bindingKind)));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (load.externalTypes.has(req.spec)) {
|
||||
continue;
|
||||
@@ -2084,38 +2314,43 @@ function preflight7(load: LoadResult): {
|
||||
if (dep) deps.push({ dep });
|
||||
}
|
||||
}
|
||||
if (!ts.isExternalModule(sf)) {
|
||||
for (const call of nestedBareRequiresOf7(sf)) {
|
||||
const spec = requireSpecOf7(call)!;
|
||||
const loc = { file: sf.fileName, start: call.getStart(sf), end: call.getEnd() };
|
||||
if (load.externalTypes.has(spec)) {
|
||||
for (const call of nestedBareRequiresOf7(sf)) {
|
||||
const spec = requireSpecOf7(call)!;
|
||||
const loc = { file: sf.fileName, start: call.getStart(sf), end: call.getEnd() };
|
||||
if (isNodeEsmFile7(sf)) {
|
||||
const bindingKind = requireCallBindingKind7(program, call);
|
||||
if (bindingKind !== "createRequire") {
|
||||
diags.push(unsupportedDiag("SC1013", loc, esmRequireFeature7(bindingKind)));
|
||||
continue;
|
||||
}
|
||||
if (!isRelativeSpecifier(spec)) {
|
||||
// --npm-static: opted-in packages ride the program-module edge
|
||||
// (the statement-level require branch above).
|
||||
const npmReq = !spec.startsWith("#") ? resolveNpmImport7(sf.fileName, spec) : null;
|
||||
if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) {
|
||||
const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile);
|
||||
if (nDep !== null) deps.push({ dep: nDep });
|
||||
continue;
|
||||
}
|
||||
if (canonicalBuiltinModule(spec) === null && !processModuleAliasRequire7(spec, null)) {
|
||||
// Binding-less by construction — same require-site throw
|
||||
// channel as the statement-level form above.
|
||||
if (probeNodeRequireRefusal(sf.fileName, spec) === null) {
|
||||
diags.push(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(spec)));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const dep = resolveImport7(program, sf, spec);
|
||||
if (dep && dep.fileName.endsWith(".json")) {
|
||||
diags.push(unsupportedDiag("SC1012", loc, "require() of JSON modules"));
|
||||
continue;
|
||||
}
|
||||
if (dep) deps.push({ dep });
|
||||
}
|
||||
if (load.externalTypes.has(spec)) {
|
||||
continue;
|
||||
}
|
||||
if (!isRelativeSpecifier(spec)) {
|
||||
// --npm-static: opted-in packages ride the program-module edge
|
||||
// (the statement-level require branch above).
|
||||
const npmReq = !spec.startsWith("#") ? resolveNpmImport7(sf.fileName, spec) : null;
|
||||
if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) {
|
||||
const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile);
|
||||
if (nDep !== null) deps.push({ dep: nDep });
|
||||
continue;
|
||||
}
|
||||
if (canonicalBuiltinModule(spec) === null && !processModuleAliasRequire7(spec, null)) {
|
||||
// Binding-less by construction — same require-site throw
|
||||
// channel as the statement-level form above.
|
||||
if (probeNodeRequireRefusal(sf.fileName, spec) === null) {
|
||||
diags.push(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(spec)));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const dep = resolveImport7(program, sf, spec);
|
||||
if (dep && dep.fileName.endsWith(".json")) {
|
||||
diags.push(unsupportedDiag("SC1012", loc, "require() of JSON modules"));
|
||||
continue;
|
||||
}
|
||||
if (dep) deps.push({ dep });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ function realpathOr(path: string): string {
|
||||
interface PkgJson {
|
||||
name?: string;
|
||||
version?: string;
|
||||
type?: string;
|
||||
types?: string;
|
||||
typings?: string;
|
||||
main?: string;
|
||||
@@ -454,6 +455,21 @@ function nearestPkgDir(dir: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** The nearest package.json FILE, whether or not its JSON parses. Runtime
|
||||
* module-format lookup must stop at a malformed nearest scope: Node reports
|
||||
* ERR_INVALID_PACKAGE_CONFIG there rather than silently inheriting a parent
|
||||
* package's `type`. The resolver's historical nearestPkgDir stays parseable-
|
||||
* package-only because its callers need a package OBJECT to inspect. */
|
||||
function nearestPackageConfigDir(dir: string): string | null {
|
||||
let d = dir;
|
||||
for (;;) {
|
||||
if (existsSync(join(d, "package.json"))) return d;
|
||||
const parent = dirname(d);
|
||||
if (parent === d) return null;
|
||||
d = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/** package.json "imports" lookup — the exports machinery over "#" keys
|
||||
* (exact keys, then '*' patterns, condition objects in key order). */
|
||||
function resolveImportsField(imports: unknown, specifier: string): string | null {
|
||||
@@ -477,6 +493,27 @@ export function nearestPkgJsonPath(fromFile: string): string | null {
|
||||
return dir === null ? null : join(dir, "package.json");
|
||||
}
|
||||
|
||||
/** The explicit module format of the nearest package scope. A package.json
|
||||
* without a recognized `type` keeps the file ambiguous, so callers may
|
||||
* apply Node's syntax detector; an explicit commonjs/module answer must win
|
||||
* over syntax detection for ordinary .js/.ts files. */
|
||||
export function nearestPackageType(fromFile: string): "module" | "commonjs" | null {
|
||||
const dir = nearestPackageConfigDir(dirname(resolve(fromFile)));
|
||||
if (dir === null) return null;
|
||||
const type = pkgJsonOf(dir)?.type;
|
||||
return type === "module" || type === "commonjs" ? type : null;
|
||||
}
|
||||
|
||||
/** The malformed nearest package scope Node would reject before loading
|
||||
* `fromFile`, or null when there is no package.json or it parses as an
|
||||
* object. Kept separate from nearestPackageType so an absent/typeless valid
|
||||
* package can still use syntax detection. */
|
||||
export function nearestInvalidPackageJsonPath(fromFile: string): string | null {
|
||||
const dir = nearestPackageConfigDir(dirname(resolve(fromFile)));
|
||||
if (dir === null || pkgJsonOf(dir) !== null) return null;
|
||||
return join(dir, "package.json");
|
||||
}
|
||||
|
||||
/** Resolves a PROJECT-INTERNAL package.json-mediated specifier from
|
||||
* `fromFile` — `#alias` (the imports field) or a self-name reference (the
|
||||
* nearest package.json's own name through its exports) — to an absolute
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
* this module.
|
||||
*
|
||||
* Placement in 7.0.2 (probed): the AST-world enums ship from unstable/ast,
|
||||
* the checker-world enums from unstable/sync. ModuleResolutionKind is the one
|
||||
* census enum the package does not re-export from any public entry point —
|
||||
* it lives in dist/enums (the #enums/* internal imports), which the exports
|
||||
* map hides from bare-specifier imports but NOT from a direct file require;
|
||||
* loadHiddenEnum resolves the package root and requires the enum module by
|
||||
* path, so the values are still TypeScript 7's own, never copied numbers. */
|
||||
* the checker-world enums from unstable/sync. ModuleResolutionKind and
|
||||
* ModuleDetectionKind are the census/options enums the package does not
|
||||
* re-export from any public entry point — they live in dist/enums (the
|
||||
* #enums/* internal imports), which the exports map hides from bare-
|
||||
* specifier imports but NOT from a direct file require; loadHiddenEnum
|
||||
* resolves the package root and requires the enum module by path, so the
|
||||
* values are still TypeScript 7's own, never copied numbers. */
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
@@ -74,6 +75,18 @@ export const ModuleResolutionKind: ModuleResolutionKindEnum =
|
||||
loadHiddenEnum<ModuleResolutionKindEnum>("moduleResolutionKind", "ModuleResolutionKind");
|
||||
export type ModuleResolutionKind = number;
|
||||
|
||||
interface ModuleDetectionKindEnum {
|
||||
readonly None: number;
|
||||
readonly Auto: number;
|
||||
readonly Legacy: number;
|
||||
readonly Force: number;
|
||||
readonly [key: string | number]: string | number;
|
||||
}
|
||||
|
||||
export const ModuleDetectionKind: ModuleDetectionKindEnum =
|
||||
loadHiddenEnum<ModuleDetectionKindEnum>("moduleDetectionKind", "ModuleDetectionKind");
|
||||
export type ModuleDetectionKind = number;
|
||||
|
||||
/** Reverse-maps a numeric enum value to its TS7 key name ("ESNext",
|
||||
* "Bundler") — the spelling tsgo's tsconfig JSON parser accepts (lowercased
|
||||
* by the caller where needed). Symbolic by construction: the name comes from
|
||||
|
||||
@@ -26,7 +26,7 @@ import type {
|
||||
} from "typescript/unstable/sync";
|
||||
import type { SourceFile } from "typescript/unstable/ast";
|
||||
import { CheckerFacade } from "./checker.js";
|
||||
import { enumKeyOf, ModuleKind, ModuleResolutionKind, ScriptTarget } from "./enums.js";
|
||||
import { enumKeyOf, ModuleDetectionKind, ModuleKind, ModuleResolutionKind, ScriptTarget } from "./enums.js";
|
||||
import { tsgoPath } from "../shared.js";
|
||||
|
||||
/** The compiler options our createProgram accepts: TS7's CompilerOptions
|
||||
@@ -61,6 +61,9 @@ function serializeOptions(options: Ts7CompilerOptions): Record<string, unknown>
|
||||
case "moduleResolution":
|
||||
out[key] = enumKeyOf(ModuleResolutionKind as never, value as number)?.toLowerCase() ?? value;
|
||||
break;
|
||||
case "moduleDetection":
|
||||
out[key] = enumKeyOf(ModuleDetectionKind as never, value as number)?.toLowerCase() ?? value;
|
||||
break;
|
||||
case "lib":
|
||||
// 5.9.3 spells lib entries "lib.es2025.d.ts"; tsconfig wants "es2025".
|
||||
out[key] = (value as string[]).map((lib) =>
|
||||
|
||||
@@ -5476,6 +5476,44 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2672-http-request-response-callback.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2672-http-request-response-callback.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2673-top-level-await-implicit-module.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2673-top-level-await-implicit-module.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2674-top-level-for-await-implicit-module.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2674-top-level-for-await-implicit-module.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2675-create-require-nested-esm.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2675-create-require-nested-esm.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2676-top-level-await-ambiguous/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2676-top-level-await-ambiguous/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2677-top-level-await-module-scope/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2677-top-level-await-module-scope/a.ts",
|
||||
"<repo>/tests/corpus/2677-top-level-await-module-scope/b.ts",
|
||||
"<repo>/tests/corpus/2677-top-level-await-module-scope/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/300-if-else.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/300-if-else.ts"
|
||||
@@ -6759,6 +6797,24 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/diagnostics/esm-syntax-commonjs/main.js": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/esm-syntax-commonjs/main.js"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1013",
|
||||
"message": "Node classifies this file as CommonJS, where this ES-module syntax marker is a SyntaxError",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/esm-syntax-commonjs/main.js",
|
||||
"start": 0,
|
||||
"end": 10
|
||||
},
|
||||
"milestone": "later",
|
||||
"hint": "use a .mjs/.mts extension or set the nearest package.json's \"type\" to \"module\""
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/exceptions.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/exceptions.ts"
|
||||
@@ -7020,6 +7076,24 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/diagnostics/invalid-package-config/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/invalid-package-config/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1013",
|
||||
"message": "Node cannot load this module because '<repo>/tests/diagnostics/invalid-package-config/package.json' is an invalid package configuration (ERR_INVALID_PACKAGE_CONFIG)",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/invalid-package-config/main.ts",
|
||||
"start": 0,
|
||||
"end": 0
|
||||
},
|
||||
"milestone": "later",
|
||||
"hint": "fix the nearest package.json so it contains valid JSON"
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/island-boundary-slots.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/island-boundary-slots.ts"
|
||||
@@ -7099,6 +7173,23 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/diagnostics/local-require-binding-esm.js": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/local-require-binding-esm.js"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1013",
|
||||
"message": "calls through a source binding named 'require' (this spelling is reserved for CommonJS/createRequire module edges — rename the binding) is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/local-require-binding-esm.js",
|
||||
"start": 80,
|
||||
"end": 100
|
||||
},
|
||||
"milestone": "later"
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/maps-surface.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/maps-surface.ts"
|
||||
@@ -7491,6 +7582,59 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/diagnostics/top-level-await-commonjs/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/top-level-await-commonjs/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1013",
|
||||
"message": "Node classifies this file as CommonJS, where this ES-module syntax marker is a SyntaxError",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/top-level-await-commonjs/main.ts",
|
||||
"start": 15,
|
||||
"end": 40
|
||||
},
|
||||
"milestone": "later",
|
||||
"hint": "use a .mjs/.mts extension or set the nearest package.json's \"type\" to \"module\""
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/top-level-await-implicit-module-nested-require.js": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/top-level-await-implicit-module-nested-require.js"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1013",
|
||||
"message": "require() in an ES module (Node throws ReferenceError — use import) is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/top-level-await-implicit-module-nested-require.js",
|
||||
"start": 114,
|
||||
"end": 134
|
||||
},
|
||||
"milestone": "later"
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/top-level-await-module-leak/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/top-level-await-module-leak/a.ts",
|
||||
"<repo>/tests/diagnostics/top-level-await-module-leak/b.ts",
|
||||
"<repo>/tests/diagnostics/top-level-await-module-leak/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'shared'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/top-level-await-module-leak/b.ts",
|
||||
"start": 37,
|
||||
"end": 43
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"<repo>/tests/diagnostics/tuples.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/tuples.ts"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const first = await Promise.resolve(20);
|
||||
const second = await Promise.resolve(22);
|
||||
|
||||
console.log(first + second);
|
||||
@@ -0,0 +1,4 @@
|
||||
let chunks = 0;
|
||||
for await (const chunk of process.stdin) chunks += chunk.length;
|
||||
|
||||
console.log(chunks);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function load() {
|
||||
// A createRequire binding is available in ESM even though Node's ambient
|
||||
// CommonJS require global is not.
|
||||
try {
|
||||
require("surely-not-installed-anywhere-pr94");
|
||||
} catch {
|
||||
return "missing";
|
||||
}
|
||||
return "SHOULD NOT PRINT";
|
||||
}
|
||||
|
||||
console.log(load());
|
||||
@@ -0,0 +1,5 @@
|
||||
// @exit: 1
|
||||
const answer = await Promise.resolve(42);
|
||||
|
||||
console.log(answer);
|
||||
process.exit(1);
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,3 @@
|
||||
const value = await Promise.resolve("a");
|
||||
|
||||
console.log(value);
|
||||
@@ -0,0 +1,3 @@
|
||||
const value = await Promise.resolve("b");
|
||||
|
||||
console.log(value);
|
||||
@@ -0,0 +1,2 @@
|
||||
import "./a.ts";
|
||||
import "./b.ts";
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export {};
|
||||
require("node:path");
|
||||
|
||||
console.log("SHOULD NOT RUN");
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
await Promise.resolve();
|
||||
console.log("SHOULD NOT RUN");
|
||||
@@ -0,0 +1,2 @@
|
||||
{
|
||||
"type": "module"
|
||||
@@ -0,0 +1,7 @@
|
||||
export {};
|
||||
|
||||
function require(specifier) {
|
||||
console.log("local", specifier);
|
||||
}
|
||||
|
||||
require("node:path");
|
||||
@@ -0,0 +1,2 @@
|
||||
const answer = await Promise.resolve(42);
|
||||
console.log(answer);
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
await Promise.resolve();
|
||||
|
||||
function load() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("node:path");
|
||||
}
|
||||
|
||||
load();
|
||||
@@ -0,0 +1,2 @@
|
||||
await Promise.resolve();
|
||||
var shared = 42;
|
||||
@@ -0,0 +1,3 @@
|
||||
await Promise.resolve();
|
||||
console.log(shared);
|
||||
console.log("SHOULD NOT RUN");
|
||||
@@ -0,0 +1,2 @@
|
||||
import "./a.ts";
|
||||
import "./b.ts";
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
esm-syntax-commonjs/main.js:1:1 - error SC1013: Node classifies this file as CommonJS, where this ES-module syntax marker is a SyntaxError
|
||||
|
||||
1 | export {};
|
||||
| ^~~~~~~~~~
|
||||
2 | require("node:path");
|
||||
|
||||
hint: use a .mjs/.mts extension or set the nearest package.json's "type" to "module"
|
||||
@@ -0,0 +1,7 @@
|
||||
invalid-package-config/main.ts:1:1 - error SC1013: Node cannot load this module because 'invalid-package-config/package.json' is an invalid package configuration (ERR_INVALID_PACKAGE_CONFIG)
|
||||
|
||||
1 | await Promise.resolve();
|
||||
| ^
|
||||
2 | console.log("SHOULD NOT RUN");
|
||||
|
||||
hint: fix the nearest package.json so it contains valid JSON
|
||||
@@ -0,0 +1,6 @@
|
||||
local-require-binding-esm.js:7:1 - error SC1013: calls through a source binding named 'require' (this spelling is reserved for CommonJS/createRequire module edges — rename the binding) is not supported yet
|
||||
|
||||
6 |
|
||||
7 | require("node:path");
|
||||
| ^~~~~~~~~~~~~~~~~~~~
|
||||
8 |
|
||||
@@ -0,0 +1,7 @@
|
||||
top-level-await-commonjs/main.ts:1:16 - error SC1013: Node classifies this file as CommonJS, where this ES-module syntax marker is a SyntaxError
|
||||
|
||||
1 | const answer = await Promise.resolve(42);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
2 | console.log(answer);
|
||||
|
||||
hint: use a .mjs/.mts extension or set the nearest package.json's "type" to "module"
|
||||
@@ -0,0 +1,6 @@
|
||||
top-level-await-implicit-module-nested-require.js:5:3 - error SC1013: require() in an ES module (Node throws ReferenceError — use import) is not supported yet
|
||||
|
||||
4 | // eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
5 | require("node:path");
|
||||
| ^~~~~~~~~~~~~~~~~~~~
|
||||
6 | }
|
||||
@@ -0,0 +1,6 @@
|
||||
top-level-await-module-leak/b.ts:2:13 - error SC0001: Cannot find name 'shared'.
|
||||
|
||||
1 | await Promise.resolve();
|
||||
2 | console.log(shared);
|
||||
| ^~~~~~
|
||||
3 | console.log("SHOULD NOT RUN");
|
||||
Reference in New Issue
Block a user