mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
feat(compiler): preserve npm static declaration overloads (#329)
This commit is contained in:
@@ -17,6 +17,7 @@ const TS5_ISLANDS = [
|
||||
// The bundler-emitted-CJS export rewrite runs inside the fs shadow,
|
||||
// BEFORE the 7.0.2 program reads the file — a text→text parser island
|
||||
// beside cjs-lexer.ts (only strings cross its boundary).
|
||||
"packages/compiler/src/frontend/npm-static-declarations.ts",
|
||||
"packages/compiler/src/frontend/npm-static-rewrite.ts",
|
||||
// Semantic cache validation parses source text only to identify exact
|
||||
// regex spans and syntax errors; its boundary is strings, offsets, and
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
applyNpmStaticDeclarationOverloads,
|
||||
npmStaticDeclarationReexports,
|
||||
npmStaticRuntimeClassTargets,
|
||||
parseNpmStaticDeclarationOverloads,
|
||||
} from "./npm-static-declarations.js";
|
||||
|
||||
const declarations = `
|
||||
export class Chainy {
|
||||
name(): string;
|
||||
name(value: string): this;
|
||||
tag(): string;
|
||||
tag(value: string): this;
|
||||
single(): string;
|
||||
unsafe(): string;
|
||||
unsafe(value: Date): this;
|
||||
generic<T>(value: T): T;
|
||||
generic(value: string): string;
|
||||
}
|
||||
`;
|
||||
|
||||
describe("npm-static declaration overload projection", () => {
|
||||
test("extracts only complete representation-safe overload groups", () => {
|
||||
const overloads = parseNpmStaticDeclarationOverloads("index.d.ts", declarations);
|
||||
expect([...overloads.keys()]).toEqual(["Chainy"]);
|
||||
expect([...overloads.get("Chainy")!.keys()]).toEqual(["name", "tag"]);
|
||||
expect(overloads.get("Chainy")!.get("name")).toEqual([
|
||||
{ parameters: [], returnType: "string" },
|
||||
{ parameters: [{ name: "value", type: "string", optional: false }], returnType: "this" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("injects overload and implementation JSDoc only into exported matching classes", () => {
|
||||
const source = `
|
||||
class Hidden {
|
||||
name(value) { return value === undefined ? "" : this; }
|
||||
}
|
||||
class Chainy {
|
||||
name(value) { return value === undefined ? "" : this; }
|
||||
tag(value) { return value === undefined ? "" : this; }
|
||||
}
|
||||
module.exports = { Chainy };
|
||||
`;
|
||||
const rewritten = applyNpmStaticDeclarationOverloads(
|
||||
"index.js",
|
||||
source,
|
||||
parseNpmStaticDeclarationOverloads("index.d.ts", declarations),
|
||||
);
|
||||
expect(rewritten).not.toBeNull();
|
||||
expect(rewritten!.insertions).toHaveLength(2);
|
||||
expect(rewritten!.text.match(/@overload/g)).toHaveLength(4);
|
||||
expect(rewritten!.text).toContain("@param {string} [value] @returns {string | Chainy}");
|
||||
expect(rewritten!.text.slice(source.indexOf("class Hidden"), source.indexOf("class Chainy"))).not.toContain("@overload");
|
||||
});
|
||||
|
||||
test("reports only relative declaration-barrel edges", () => {
|
||||
expect(npmStaticDeclarationReexports("esm.d.mts", `
|
||||
export * from "./index.js";
|
||||
export { Type } from "./types.js";
|
||||
export * from "other-package";
|
||||
`)).toEqual(["./index.js", "./types.js"]);
|
||||
});
|
||||
|
||||
test("binds declaration classes to direct and one-hop runtime exports", () => {
|
||||
expect(npmStaticRuntimeClassTargets("index.js", `
|
||||
const { Command, Other: Alias } = require("./lib/command.js");
|
||||
class Local {}
|
||||
exports.Command = Command;
|
||||
exports.Alias = Alias;
|
||||
exports.Local = Local;
|
||||
`, new Set(["Command", "Alias", "Local"]))).toEqual(new Map([
|
||||
["Command", "./lib/command.js"],
|
||||
["Local", null],
|
||||
]));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,402 @@
|
||||
/* Declaration-overload projection for --npm-static. The runtime program
|
||||
* still resolves to and compiles package JavaScript, but an authored .d.ts
|
||||
* can carry overloads inference cannot reproduce (the common getter/setter
|
||||
* shape `name(): string` / `name(value): this`). This string-bounded
|
||||
* TypeScript 5 parser island extracts only complete, representation-safe
|
||||
* groups and respells them as JSDoc immediately before the matching
|
||||
* exported JavaScript class method. TypeScript 7 then checks and lowers one
|
||||
* world: implementation bodies remain the runtime truth, while overload
|
||||
* calls get the package author's more precise signature. */
|
||||
|
||||
import ts from "typescript5";
|
||||
|
||||
export interface NpmStaticOverloadParameter {
|
||||
name: string;
|
||||
type: string;
|
||||
optional: boolean;
|
||||
}
|
||||
|
||||
export interface NpmStaticOverloadSignature {
|
||||
parameters: readonly NpmStaticOverloadParameter[];
|
||||
returnType: string;
|
||||
}
|
||||
|
||||
export type NpmStaticDeclarationOverloads = ReadonlyMap<
|
||||
string,
|
||||
ReadonlyMap<string, readonly NpmStaticOverloadSignature[]>
|
||||
>;
|
||||
|
||||
export interface NpmStaticOverloadRewrite {
|
||||
text: string;
|
||||
insertions: readonly { offset: number; length: number }[];
|
||||
}
|
||||
|
||||
const SAFE_KEYWORD_TYPES = new Set<ts.SyntaxKind>([
|
||||
ts.SyntaxKind.BooleanKeyword,
|
||||
ts.SyntaxKind.NeverKeyword,
|
||||
ts.SyntaxKind.NullKeyword,
|
||||
ts.SyntaxKind.NumberKeyword,
|
||||
ts.SyntaxKind.StringKeyword,
|
||||
ts.SyntaxKind.UndefinedKeyword,
|
||||
ts.SyntaxKind.VoidKeyword,
|
||||
]);
|
||||
|
||||
function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
|
||||
return ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === kind);
|
||||
}
|
||||
|
||||
function safeTypeText(node: ts.TypeNode, sourceFile: ts.SourceFile, className: string): string | null {
|
||||
if (SAFE_KEYWORD_TYPES.has(node.kind) || ts.isThisTypeNode(node)) return node.getText(sourceFile);
|
||||
if (ts.isParenthesizedTypeNode(node)) {
|
||||
const inner = safeTypeText(node.type, sourceFile, className);
|
||||
return inner === null ? null : `(${inner})`;
|
||||
}
|
||||
if (ts.isArrayTypeNode(node)) {
|
||||
const element = safeTypeText(node.elementType, sourceFile, className);
|
||||
return element === null ? null : `${element}[]`;
|
||||
}
|
||||
if (ts.isUnionTypeNode(node)) {
|
||||
const arms = node.types.map((type) => safeTypeText(type, sourceFile, className));
|
||||
return arms.some((arm) => arm === null) ? null : arms.join(" | ");
|
||||
}
|
||||
return ts.isTypeReferenceNode(node) &&
|
||||
ts.isIdentifier(node.typeName) &&
|
||||
node.typeName.text === className &&
|
||||
(node.typeArguments?.length ?? 0) === 0
|
||||
? className
|
||||
: null;
|
||||
}
|
||||
|
||||
function overloadSignature(
|
||||
sourceFile: ts.SourceFile,
|
||||
className: string,
|
||||
method: ts.MethodDeclaration,
|
||||
): NpmStaticOverloadSignature | null {
|
||||
if (
|
||||
!ts.isIdentifier(method.name) ||
|
||||
method.type === undefined ||
|
||||
(method.typeParameters?.length ?? 0) !== 0 ||
|
||||
hasModifier(method, ts.SyntaxKind.StaticKeyword) ||
|
||||
hasModifier(method, ts.SyntaxKind.PrivateKeyword) ||
|
||||
hasModifier(method, ts.SyntaxKind.ProtectedKeyword)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const returnType = safeTypeText(method.type, sourceFile, className);
|
||||
if (returnType === null) return null;
|
||||
const parameters: NpmStaticOverloadParameter[] = [];
|
||||
for (const parameter of method.parameters) {
|
||||
if (
|
||||
!ts.isIdentifier(parameter.name) ||
|
||||
parameter.name.text === "this" ||
|
||||
parameter.type === undefined ||
|
||||
parameter.initializer !== undefined ||
|
||||
parameter.dotDotDotToken !== undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const type = safeTypeText(parameter.type, sourceFile, className);
|
||||
if (type === null) return null;
|
||||
parameters.push({
|
||||
name: parameter.name.text,
|
||||
type,
|
||||
optional: parameter.questionToken !== undefined,
|
||||
});
|
||||
}
|
||||
return { parameters, returnType };
|
||||
}
|
||||
|
||||
/** Extracts complete safe overload groups from exported non-generic classes. */
|
||||
export function parseNpmStaticDeclarationOverloads(
|
||||
declarationPath: string,
|
||||
source: string,
|
||||
): NpmStaticDeclarationOverloads {
|
||||
const sourceFile = ts.createSourceFile(declarationPath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
||||
const classes = new Map<string, ReadonlyMap<string, readonly NpmStaticOverloadSignature[]>>();
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (
|
||||
!ts.isClassDeclaration(statement) ||
|
||||
statement.name === undefined ||
|
||||
(statement.typeParameters?.length ?? 0) !== 0 ||
|
||||
!hasModifier(statement, ts.SyntaxKind.ExportKeyword) ||
|
||||
hasModifier(statement, ts.SyntaxKind.DefaultKeyword)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const className = statement.name.text;
|
||||
const groups = new Map<string, ts.MethodDeclaration[]>();
|
||||
for (const member of statement.members) {
|
||||
if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) continue;
|
||||
const group = groups.get(member.name.text) ?? [];
|
||||
group.push(member);
|
||||
groups.set(member.name.text, group);
|
||||
}
|
||||
const overloads = new Map<string, readonly NpmStaticOverloadSignature[]>();
|
||||
for (const [name, methods] of groups) {
|
||||
if (methods.length < 2) continue;
|
||||
const signatures = methods.map((method) => overloadSignature(sourceFile, className, method));
|
||||
// A partial set could select the wrong branch. Keep inference when
|
||||
// any authored signature is outside the projection's safe grammar.
|
||||
if (signatures.some((signature) => signature === null)) continue;
|
||||
overloads.set(name, signatures as NpmStaticOverloadSignature[]);
|
||||
}
|
||||
if (overloads.size > 0) classes.set(className, overloads);
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
/** Relative declaration-barrel edges whose target stays subject to the
|
||||
* caller's package-bounded resolution. Bare type dependencies deliberately
|
||||
* do not inherit the opted package's declaration trust. */
|
||||
export function npmStaticDeclarationReexports(
|
||||
declarationPath: string,
|
||||
source: string,
|
||||
): readonly string[] {
|
||||
const sourceFile = ts.createSourceFile(declarationPath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
||||
return sourceFile.statements.flatMap((statement) =>
|
||||
ts.isExportDeclaration(statement) &&
|
||||
statement.moduleSpecifier !== undefined &&
|
||||
ts.isStringLiteral(statement.moduleSpecifier) &&
|
||||
statement.moduleSpecifier.text.startsWith(".")
|
||||
? [statement.moduleSpecifier.text]
|
||||
: []
|
||||
);
|
||||
}
|
||||
|
||||
function requireSpecifier(expression: ts.Expression | undefined): string | null {
|
||||
const argument = expression !== undefined && ts.isCallExpression(expression) ? expression.arguments[0] : undefined;
|
||||
return expression !== undefined &&
|
||||
ts.isCallExpression(expression) &&
|
||||
ts.isIdentifier(expression.expression) &&
|
||||
expression.expression.text === "require" &&
|
||||
expression.arguments.length === 1 &&
|
||||
argument !== undefined &&
|
||||
ts.isStringLiteralLike(argument)
|
||||
? argument.text
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Maps declaration class names to their implementation edge from one
|
||||
* runtime package entry. Null means the class is declared in the entry;
|
||||
* a string is a relative re-export target. Multi-hop and aliased class
|
||||
* re-exports stay out of the first safe slice. */
|
||||
export function npmStaticRuntimeClassTargets(
|
||||
sourcePath: string,
|
||||
source: string,
|
||||
classNames: ReadonlySet<string>,
|
||||
): ReadonlyMap<string, string | null> {
|
||||
const sourceFile = ts.createSourceFile(sourcePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
||||
const localClasses = new Set(
|
||||
sourceFile.statements.flatMap((statement) =>
|
||||
ts.isClassDeclaration(statement) && statement.name !== undefined ? [statement.name.text] : []
|
||||
),
|
||||
);
|
||||
const required = new Map<string, { imported: string; specifier: string }>();
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (!ts.isVariableStatement(statement)) continue;
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
const specifier = requireSpecifier(declaration.initializer);
|
||||
if (specifier === null || !specifier.startsWith(".") || !ts.isObjectBindingPattern(declaration.name)) continue;
|
||||
for (const element of declaration.name.elements) {
|
||||
if (element.dotDotDotToken !== undefined || !ts.isIdentifier(element.name)) continue;
|
||||
const imported = element.propertyName !== undefined && ts.isIdentifier(element.propertyName)
|
||||
? element.propertyName.text
|
||||
: element.name.text;
|
||||
required.set(element.name.text, { imported, specifier });
|
||||
}
|
||||
}
|
||||
}
|
||||
const targets = new Map<string, string | null>();
|
||||
const record = (exported: string, local: string, specifier?: string): void => {
|
||||
if (!classNames.has(exported) || exported !== local || targets.has(exported)) return;
|
||||
if (specifier !== undefined) {
|
||||
targets.set(exported, specifier);
|
||||
return;
|
||||
}
|
||||
const imported = required.get(local);
|
||||
if (imported !== undefined && imported.imported === exported) targets.set(exported, imported.specifier);
|
||||
else if (localClasses.has(local)) targets.set(exported, null);
|
||||
};
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (
|
||||
ts.isClassDeclaration(statement) &&
|
||||
statement.name !== undefined &&
|
||||
hasModifier(statement, ts.SyntaxKind.ExportKeyword)
|
||||
) {
|
||||
record(statement.name.text, statement.name.text);
|
||||
continue;
|
||||
}
|
||||
if (ts.isExportDeclaration(statement) && statement.exportClause !== undefined && ts.isNamedExports(statement.exportClause)) {
|
||||
const specifier = statement.moduleSpecifier !== undefined && ts.isStringLiteral(statement.moduleSpecifier)
|
||||
? statement.moduleSpecifier.text
|
||||
: undefined;
|
||||
if (specifier !== undefined && !specifier.startsWith(".")) continue;
|
||||
for (const element of statement.exportClause.elements) {
|
||||
record(element.name.text, element.propertyName?.text ?? element.name.text, specifier);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression)) continue;
|
||||
const { left, right, operatorToken } = statement.expression;
|
||||
if (operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(left) &&
|
||||
ts.isIdentifier(right) &&
|
||||
((ts.isIdentifier(left.expression) && left.expression.text === "exports") ||
|
||||
(ts.isPropertyAccessExpression(left.expression) &&
|
||||
ts.isIdentifier(left.expression.expression) &&
|
||||
left.expression.expression.text === "module" &&
|
||||
left.expression.name.text === "exports"))
|
||||
) {
|
||||
record(left.name.text, right.text);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
ts.isPropertyAccessExpression(left) &&
|
||||
ts.isIdentifier(left.expression) &&
|
||||
left.expression.text === "module" &&
|
||||
left.name.text === "exports" &&
|
||||
ts.isObjectLiteralExpression(right)
|
||||
) {
|
||||
for (const property of right.properties) {
|
||||
if (ts.isShorthandPropertyAssignment(property)) record(property.name.text, property.name.text);
|
||||
else if (
|
||||
ts.isPropertyAssignment(property) &&
|
||||
ts.isIdentifier(property.name) &&
|
||||
ts.isIdentifier(property.initializer)
|
||||
) {
|
||||
record(property.name.text, property.initializer.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
function exportedClassNames(sourceFile: ts.SourceFile): ReadonlySet<string> {
|
||||
const names = new Set<string>();
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (
|
||||
ts.isClassDeclaration(statement) &&
|
||||
statement.name !== undefined &&
|
||||
hasModifier(statement, ts.SyntaxKind.ExportKeyword)
|
||||
) {
|
||||
names.add(statement.name.text);
|
||||
continue;
|
||||
}
|
||||
if (ts.isExportDeclaration(statement) && statement.exportClause !== undefined && ts.isNamedExports(statement.exportClause)) {
|
||||
for (const element of statement.exportClause.elements) names.add(element.propertyName?.text ?? element.name.text);
|
||||
continue;
|
||||
}
|
||||
if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression)) continue;
|
||||
const { left, right, operatorToken } = statement.expression;
|
||||
if (operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(left) &&
|
||||
ts.isIdentifier(right) &&
|
||||
((ts.isIdentifier(left.expression) && left.expression.text === "exports") ||
|
||||
(ts.isPropertyAccessExpression(left.expression) &&
|
||||
ts.isIdentifier(left.expression.expression) &&
|
||||
left.expression.expression.text === "module" &&
|
||||
left.expression.name.text === "exports")) &&
|
||||
left.name.text === right.text
|
||||
) {
|
||||
names.add(right.text);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
ts.isPropertyAccessExpression(left) &&
|
||||
ts.isIdentifier(left.expression) &&
|
||||
left.expression.text === "module" &&
|
||||
left.name.text === "exports" &&
|
||||
ts.isObjectLiteralExpression(right)
|
||||
) {
|
||||
for (const property of right.properties) {
|
||||
if (ts.isShorthandPropertyAssignment(property)) names.add(property.name.text);
|
||||
else if (
|
||||
ts.isPropertyAssignment(property) &&
|
||||
ts.isIdentifier(property.name) &&
|
||||
ts.isIdentifier(property.initializer) &&
|
||||
property.name.text === property.initializer.text
|
||||
) {
|
||||
names.add(property.name.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function overloadComment(signature: NpmStaticOverloadSignature): string {
|
||||
const params = signature.parameters.map((parameter) =>
|
||||
`@param {${parameter.type}} ${parameter.optional ? `[${parameter.name}]` : parameter.name}`
|
||||
);
|
||||
return `/** @overload ${params.join(" ")} @returns {${signature.returnType}} */`;
|
||||
}
|
||||
|
||||
function implementationComment(
|
||||
className: string,
|
||||
method: ts.MethodDeclaration,
|
||||
signatures: readonly NpmStaticOverloadSignature[],
|
||||
): string | null {
|
||||
if (method.parameters.some((parameter) => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken !== undefined)) return null;
|
||||
const maxParams = Math.max(...signatures.map((signature) => signature.parameters.length));
|
||||
if (method.parameters.length !== maxParams) return null;
|
||||
const params: string[] = [];
|
||||
for (let index = 0; index < maxParams; index++) {
|
||||
const types = [...new Set(signatures.flatMap((signature) => signature.parameters[index]?.type ?? []))];
|
||||
if (types.length === 0) return null;
|
||||
const parameter = method.parameters[index];
|
||||
if (parameter === undefined || !ts.isIdentifier(parameter.name)) return null;
|
||||
const name = parameter.name.text;
|
||||
const optional = signatures.some((signature) => {
|
||||
const candidate = signature.parameters[index];
|
||||
return candidate === undefined || candidate.optional;
|
||||
});
|
||||
params.push(`@param {${types.join(" | ")}} ${optional ? `[${name}]` : name}`);
|
||||
}
|
||||
const returns = [...new Set(signatures.map((signature) =>
|
||||
signature.returnType.replace(/\bthis\b/g, className)
|
||||
))];
|
||||
return `/** ${params.join(" ")} @returns {${returns.join(" | ")}} */`;
|
||||
}
|
||||
|
||||
/** Injects declaration overload JSDoc into matching exported JS classes. */
|
||||
export function applyNpmStaticDeclarationOverloads(
|
||||
sourcePath: string,
|
||||
source: string,
|
||||
declarations: NpmStaticDeclarationOverloads,
|
||||
): NpmStaticOverloadRewrite | null {
|
||||
if (declarations.size === 0) return null;
|
||||
const sourceFile = ts.createSourceFile(sourcePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
||||
const exported = exportedClassNames(sourceFile);
|
||||
const inserts: { offset: number; text: string }[] = [];
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (!ts.isClassDeclaration(statement) || statement.name === undefined || !exported.has(statement.name.text)) continue;
|
||||
const classOverloads = declarations.get(statement.name.text);
|
||||
if (classOverloads === undefined) continue;
|
||||
for (const member of statement.members) {
|
||||
if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name) || member.body === undefined) continue;
|
||||
const signatures = classOverloads.get(member.name.text);
|
||||
if (signatures === undefined) continue;
|
||||
const jsDocs = (member as ts.MethodDeclaration & { jsDoc?: readonly ts.JSDoc[] }).jsDoc ?? [];
|
||||
if (jsDocs.some((doc) => source.slice(doc.pos, doc.end).includes("@overload"))) continue;
|
||||
const implementation = jsDocs.length === 0 ? implementationComment(statement.name.text, member, signatures) : null;
|
||||
if (jsDocs.length === 0 && implementation === null) continue;
|
||||
const offset = jsDocs[0]?.getStart(sourceFile) ?? member.getStart(sourceFile);
|
||||
const text = `${signatures.map(overloadComment).join(" ")} ${implementation === null ? "" : implementation + " "}`;
|
||||
inserts.push({ offset, text });
|
||||
}
|
||||
}
|
||||
if (inserts.length === 0) return null;
|
||||
let text = source;
|
||||
for (const insert of [...inserts].sort((a, b) => b.offset - a.offset)) {
|
||||
text = text.slice(0, insert.offset) + insert.text + text.slice(insert.offset);
|
||||
}
|
||||
return {
|
||||
text,
|
||||
insertions: inserts
|
||||
.sort((a, b) => a.offset - b.offset)
|
||||
.map((insert) => ({ offset: insert.offset, length: insert.text.length })),
|
||||
};
|
||||
}
|
||||
@@ -8,14 +8,14 @@
|
||||
* included) types the bodies, and every statement the lowering cannot
|
||||
* honor becomes the standard JS runtime fence (the trap throws AT the
|
||||
* statement if the program ever drives it — trust-but-verify, never
|
||||
* silent trust). The .d.ts is deliberately DROPPED from the opted-in
|
||||
* package's resolution: a declaration is a CLAIM about the body, and the
|
||||
* compiled artifact must be built from what the body provably is, not
|
||||
* from what the declaration says it should be. Program-side use sites
|
||||
* therefore typecheck against the INFERRED export surface; where the
|
||||
* package's own JSDoc (usually written against that same .d.ts) types a
|
||||
* boundary, the declared types flow in through inference and the runtime
|
||||
* fences guard the sites the checker could not prove.
|
||||
* silent trust). The .d.ts is deliberately DROPPED from module resolution:
|
||||
* a declaration is a CLAIM about the body, and the compiled artifact must
|
||||
* be built from what the body provably is, not from declaration-only
|
||||
* values. One bounded part of that claim survives: complete non-generic
|
||||
* overload groups over representation-safe types project as JSDoc onto
|
||||
* their matching exported runtime class. Calls retain authored overload
|
||||
* precision while the implementation body, its union ABI, and every
|
||||
* runtime fence still come from JavaScript.
|
||||
*
|
||||
* MECHANISM. Resolution is the single lever:
|
||||
* - tsgo's server-side resolution reads package.json and probes sibling
|
||||
@@ -26,6 +26,10 @@
|
||||
* @types twin is hidden whole — the bundler resolution then lands on
|
||||
* the shipped JS, which allowJs + maxNodeModuleJsDepth admit into the
|
||||
* program as checkable source.
|
||||
* - Before that shadow is enabled, the package's own declaration entry
|
||||
* and relative declaration barrels are scanned for safe overload groups.
|
||||
* The runtime entry binds each class to its direct or one-hop re-export
|
||||
* file; only that file receives the generated JSDoc projection.
|
||||
* - scriptc's own resolver (resolve.ts) mirrors the same answer: for an
|
||||
* opted-in package the types pass is skipped, the "types" export
|
||||
* condition is dropped, and the @types mangling never runs.
|
||||
@@ -56,10 +60,13 @@
|
||||
|
||||
import { dirname } from "node:path";
|
||||
import { rewriteBundlerCjsExports } from "./npm-static-rewrite.js";
|
||||
import { applyNpmStaticDeclarationOverloads } from "./npm-static-declarations.js";
|
||||
import type { NpmStaticDeclarationOverloads } from "./npm-static-declarations.js";
|
||||
import { npmPackageNameOf, registerWorkspacePackage, workspacePackageOfPath } from "./workspace-registry.js";
|
||||
import { trackedExists, trackedReadFile, trackedRealpath } from "./input-tracker.js";
|
||||
|
||||
let activePackages: ReadonlySet<string> = new Set();
|
||||
let declarationOverloads: ReadonlyMap<string, NpmStaticDeclarationOverloads> = new Map();
|
||||
|
||||
/** Offender records of the CURRENT load attempt: packages whose static
|
||||
* compilation the preflight had to refuse, with the first reason. The
|
||||
@@ -72,12 +79,20 @@ const rewriteCache = new Map<string, string | null>();
|
||||
|
||||
export function setNpmStaticPackages(packages: Iterable<string>): void {
|
||||
activePackages = new Set(packages);
|
||||
declarationOverloads = new Map();
|
||||
offenders.clear();
|
||||
rewriteCache.clear();
|
||||
untypedPkgCache.clear();
|
||||
realpathProbed.clear();
|
||||
}
|
||||
|
||||
export function setNpmStaticDeclarationOverloads(
|
||||
overloads: ReadonlyMap<string, NpmStaticDeclarationOverloads>,
|
||||
): void {
|
||||
declarationOverloads = overloads;
|
||||
rewriteCache.clear();
|
||||
}
|
||||
|
||||
export function npmStaticActive(): boolean {
|
||||
return activePackages.size > 0;
|
||||
}
|
||||
@@ -373,11 +388,16 @@ export function npmStaticFsShadow(): NpmStaticFsShadow | null {
|
||||
try {
|
||||
const source = trackedReadFile(path);
|
||||
if (source !== null) {
|
||||
const answer = rewriteBundlerCjsExports(source, path);
|
||||
const projected = applyNpmStaticDeclarationOverloads(
|
||||
path,
|
||||
source,
|
||||
declarationOverloads.get(path.split("\\").join("/")) ?? new Map(),
|
||||
);
|
||||
const answer = rewriteBundlerCjsExports(projected?.text ?? source, path);
|
||||
if (answer !== null && typeof answer === "object") {
|
||||
reportNpmStaticOffender(target.pkg, answer.degrade);
|
||||
} else {
|
||||
rewritten = answer;
|
||||
rewritten = answer ?? projected?.text ?? null;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -52,7 +52,9 @@ import {
|
||||
} from "../diagnostics/diagnostic.js";
|
||||
import { isNodeModulesPath, nearestInvalidPackageJsonPath, nearestPackageType, nearestPkgJsonPath, projectDtsRuntimeSibling, resolveBareModule, resolveProjectModule, resolveTypeDirective, setProjectPathMappings, setProjectRealm } from "./resolve.js";
|
||||
import { probeNodeImportRefusal, probeNodeRequireRefusal } from "./npm.js";
|
||||
import { isNpmStaticPackage, npmStaticActive, npmStaticFsShadow, npmStaticPackageOfPath, reportNpmStaticOffender, setNpmStaticPackages } from "./npm-static.js";
|
||||
import { isNpmStaticPackage, npmStaticActive, npmStaticFsShadow, npmStaticPackageOfPath, reportNpmStaticOffender, setNpmStaticDeclarationOverloads, setNpmStaticPackages } from "./npm-static.js";
|
||||
import { npmStaticDeclarationReexports, npmStaticRuntimeClassTargets, parseNpmStaticDeclarationOverloads } from "./npm-static-declarations.js";
|
||||
import type { NpmStaticDeclarationOverloads, NpmStaticOverloadSignature } from "./npm-static-declarations.js";
|
||||
import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js";
|
||||
import { cjsLexerVisibleNames } from "./cjs-lexer.js";
|
||||
import {
|
||||
@@ -72,6 +74,7 @@ import {
|
||||
clearWorkspacePackages,
|
||||
isRelativeSpecifier,
|
||||
isWorkspacePackageName,
|
||||
npmPackageNameOf,
|
||||
registerWorkspacePackage,
|
||||
workspacePackageOfPath,
|
||||
} from "./workspace-registry.js";
|
||||
@@ -81,7 +84,7 @@ import {
|
||||
JS_ANY_OPERATOR_CODES,
|
||||
JS_RELAXED_TSC_CODES,
|
||||
} from "./tsc-codes.js";
|
||||
import { trackedFileExists } from "./input-tracker.js";
|
||||
import { trackedFileExists, trackedReadFile, trackedRealpath } from "./input-tracker.js";
|
||||
|
||||
const BASE_OPTIONS: ts.Ts7CompilerOptions = {
|
||||
strict: true,
|
||||
@@ -248,6 +251,73 @@ export interface LoadResult {
|
||||
projectWorld: () => ts.Program;
|
||||
}
|
||||
|
||||
function declarationReexportTarget(fromFile: string, specifier: string): string | null {
|
||||
const base = resolve(dirname(fromFile), specifier);
|
||||
const candidates: string[] = [];
|
||||
const extension = /\.(mjs|cjs|js)$/.exec(base)?.[1];
|
||||
if (extension !== undefined) {
|
||||
const stem = base.slice(0, -(extension.length + 1));
|
||||
if (extension === "mjs") candidates.push(`${stem}.d.mts`, `${stem}.d.ts`);
|
||||
else if (extension === "cjs") candidates.push(`${stem}.d.cts`, `${stem}.d.ts`);
|
||||
else candidates.push(`${stem}.d.ts`, `${stem}.d.mts`, `${stem}.d.cts`);
|
||||
} else {
|
||||
candidates.push(base, `${base}.d.ts`, `${base}.d.mts`, `${base}.d.cts`);
|
||||
}
|
||||
candidates.push(
|
||||
resolve(base, "index.d.ts"),
|
||||
resolve(base, "index.d.mts"),
|
||||
resolve(base, "index.d.cts"),
|
||||
);
|
||||
return candidates.find((candidate) => trackedFileExists(candidate)) ?? null;
|
||||
}
|
||||
|
||||
function runtimeReexportTarget(fromFile: string, specifier: string): string | null {
|
||||
const base = resolve(dirname(fromFile), specifier);
|
||||
const candidates = [
|
||||
base,
|
||||
`${base}.js`,
|
||||
`${base}.mjs`,
|
||||
`${base}.cjs`,
|
||||
resolve(base, "index.js"),
|
||||
resolve(base, "index.mjs"),
|
||||
resolve(base, "index.cjs"),
|
||||
];
|
||||
const target = candidates.find((candidate) => trackedFileExists(candidate) && isJsSourceFileName(candidate));
|
||||
return target === undefined ? null : (trackedRealpath(target) ?? target);
|
||||
}
|
||||
|
||||
/** Loads an opted package's own declaration entry and its relative barrel
|
||||
* closure. This is intentionally narrower than TypeScript module
|
||||
* resolution: bare edges name other packages and never inherit trust. */
|
||||
function npmStaticDeclarationOverloadsOf(entryPath: string): NpmStaticDeclarationOverloads {
|
||||
const classes = new Map<string, Map<string, readonly NpmStaticOverloadSignature[]>>();
|
||||
const seen = new Set<string>();
|
||||
const packageJson = nearestPkgJsonPath(entryPath);
|
||||
const packageRoot = (packageJson === null ? dirname(entryPath) : dirname(packageJson)).split("\\").join("/");
|
||||
const visit = (path: string): void => {
|
||||
path = resolve(path);
|
||||
const normalized = path.split("\\").join("/");
|
||||
if (normalized !== packageRoot && !normalized.startsWith(packageRoot + "/")) return;
|
||||
if (seen.has(path)) return;
|
||||
seen.add(path);
|
||||
const source = trackedReadFile(path);
|
||||
if (source === null) return;
|
||||
for (const [className, methods] of parseNpmStaticDeclarationOverloads(path, source)) {
|
||||
const target = classes.get(className) ?? new Map<string, readonly NpmStaticOverloadSignature[]>();
|
||||
classes.set(className, target);
|
||||
for (const [methodName, signatures] of methods) {
|
||||
if (!target.has(methodName)) target.set(methodName, signatures);
|
||||
}
|
||||
}
|
||||
for (const specifier of npmStaticDeclarationReexports(path, source)) {
|
||||
const target = declarationReexportTarget(path, specifier);
|
||||
if (target !== null) visit(target);
|
||||
}
|
||||
};
|
||||
visit(entryPath);
|
||||
return classes;
|
||||
}
|
||||
|
||||
/** See LoadResult.startupCrash: Node's exact error message, the IR error
|
||||
* class that carries it (a RUNTIME_ERROR_CLASSES name — %Error for the
|
||||
* resolver's ERR_MODULE_NOT_FOUND family, %TypeError for invalid-specifier
|
||||
@@ -447,7 +517,45 @@ export function loadProgram(
|
||||
}
|
||||
externalTypes.set(specifier, declarationPath);
|
||||
}
|
||||
setNpmStaticPackages(opts?.npmStatic ?? []);
|
||||
const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])];
|
||||
// Resolve authored declaration entries before enabling the JS-only
|
||||
// package resolver. Safe overload metadata binds through the runtime
|
||||
// entry to one exact implementation file; it never contributes values
|
||||
// or executable module edges.
|
||||
const declarationOverloads = new Map<string, NpmStaticDeclarationOverloads>();
|
||||
for (const pkg of npmStaticPackages) {
|
||||
const resolved = resolveBareModule(entryPath, pkg, "types-only");
|
||||
if (
|
||||
resolved === null ||
|
||||
resolved.packageName !== pkg ||
|
||||
!/\.d\.(?:ts|mts|cts)$/.test(resolved.typesFile)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const overloads = npmStaticDeclarationOverloadsOf(resolved.typesFile);
|
||||
if (overloads.size === 0) continue;
|
||||
const runtime = resolveBareModule(entryPath, pkg, "js-only");
|
||||
if (runtime === null || !isJsSourceFileName(runtime.typesFile)) continue;
|
||||
const runtimeSource = trackedReadFile(runtime.typesFile);
|
||||
if (runtimeSource === null) continue;
|
||||
const targets = npmStaticRuntimeClassTargets(runtime.typesFile, runtimeSource, new Set(overloads.keys()));
|
||||
for (const [className, specifier] of targets) {
|
||||
const methods = overloads.get(className);
|
||||
if (methods === undefined) continue;
|
||||
const target = specifier === null ? runtime.typesFile : runtimeReexportTarget(runtime.typesFile, specifier);
|
||||
if (target === null) continue;
|
||||
const targetPackage = npmPackageNameOf(target);
|
||||
const targetNorm = target.split("\\").join("/");
|
||||
const insideWorkspace = runtime.workspaceDir !== undefined &&
|
||||
targetNorm.startsWith(runtime.workspaceDir.split("\\").join("/") + "/");
|
||||
if (targetPackage !== pkg && !insideWorkspace) continue;
|
||||
const byClass = new Map(declarationOverloads.get(targetNorm) ?? []);
|
||||
byClass.set(className, methods);
|
||||
declarationOverloads.set(targetNorm, byClass);
|
||||
}
|
||||
}
|
||||
setNpmStaticPackages(npmStaticPackages);
|
||||
setNpmStaticDeclarationOverloads(declarationOverloads);
|
||||
// Workspace-package registrations reset per load (same discipline as the
|
||||
// npm-static set), then the opted-in names are probed UP FRONT: a
|
||||
// workspace-linked opt-in resolves to files whose realpaths carry no
|
||||
|
||||
@@ -766,8 +766,10 @@ export function resolveBareModule(
|
||||
fromFile: string,
|
||||
specifier: string,
|
||||
/** "js-only" forces the runtime-JS resolution regardless of the active
|
||||
* --npm-static set (the auto-detection probe); default follows the set. */
|
||||
mode?: "js-only",
|
||||
* --npm-static set (the auto-detection probe); "types-only" forces the
|
||||
* unshadowed declaration pass for overload-overlay discovery; default
|
||||
* follows the active set. */
|
||||
mode?: "js-only" | "types-only",
|
||||
): BareResolution | null {
|
||||
const pkgName = packageNameOfSpecifier(specifier);
|
||||
const rest = specifier.slice(pkgName.length).replace(/^\//, "");
|
||||
@@ -775,7 +777,7 @@ export function resolveBareModule(
|
||||
// An opted-in --npm-static package resolves to its RUNTIME JS: the js
|
||||
// pass only, the "types" export condition dropped, the @types mangling
|
||||
// never consulted — mirroring the shadowed world the tsgo host serves.
|
||||
const npmStatic = mode === "js-only" || isNpmStaticPackage(pkgName);
|
||||
const npmStatic = mode === "js-only" || (mode !== "types-only" && isNpmStaticPackage(pkgName));
|
||||
const conditions = npmStatic ? JS_ONLY_CONDITIONS : EXPORT_CONDITIONS;
|
||||
|
||||
const inPackage = (nmPkgDir: string, name: string, pass: ResolutionPass): BareResolution | null => {
|
||||
@@ -859,6 +861,7 @@ export function resolveBareModule(
|
||||
}
|
||||
};
|
||||
|
||||
if (mode === "types-only") return passOnce("types");
|
||||
return npmStatic ? passOnce("js") : (passOnce("types") ?? passOnce("js"));
|
||||
}
|
||||
|
||||
|
||||
@@ -790,11 +790,11 @@ function runFrontend(
|
||||
preflight = checkPreflight(load);
|
||||
}
|
||||
// The last resort, ALL modes: an opt-in can change the PROGRAM's OWN
|
||||
// typecheck through errors that name no package at all (the inferred
|
||||
// surface replaces the shipped .d.ts — the commander name()/description()
|
||||
// chaining shape, or a .d.ts type-GUARD an inferred JS function cannot
|
||||
// reproduce, so every catch-clause narrowing site reports "'err' is of
|
||||
// type 'unknown'"). Those SC0001s anchor in USER files no offender or
|
||||
// typecheck through errors that name no package at all (declaration
|
||||
// shapes outside the safe overload projection — generic overloads,
|
||||
// declaration-only members, or a .d.ts type-GUARD an inferred JS
|
||||
// function cannot reproduce). Those SC0001s anchor in USER files no
|
||||
// offender or
|
||||
// message attribution reaches, so each remaining package is probed
|
||||
// ALONE-dropped (n is the opt-in count — a handful of extra analysis
|
||||
// loads); culprits whose removal clears the errors fall back with a
|
||||
|
||||
@@ -10,6 +10,7 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const ALLOWED_TYPESCRIPT5_IMPORTS = new Set([
|
||||
"packages/compiler/src/frontend/cjs-lexer.ts",
|
||||
"packages/compiler/src/frontend/lowering/lower-comptime.ts",
|
||||
"packages/compiler/src/frontend/npm-static-declarations.ts",
|
||||
"packages/compiler/src/frontend/npm-static-rewrite.ts",
|
||||
"packages/compiler/src/frontend/npm.ts",
|
||||
"packages/compiler/src/frontend/provenance.ts",
|
||||
|
||||
+6
-6
@@ -1,15 +1,15 @@
|
||||
// The --npm-static commander probe: the same CLI shape as calc.ts, written
|
||||
// against commander's INFERRED surface (no .d.ts in the program — the
|
||||
// getter/setter JSDoc unions mean no chaining through name()/description(),
|
||||
// and action callbacks annotate their own params). npm-static.test.ts
|
||||
// package declarations provide safe getter/setter overloads while action
|
||||
// callbacks annotate their own params). npm-static.test.ts
|
||||
// asserts the coverage numbers; the binary builds fully static and traps at
|
||||
// the first driven runtime fence. Implicit-any monomorphization carried the
|
||||
// typed-value → untyped-param boundary (_registerCommand and its local
|
||||
// knownBy helper instantiate per argument types now); the frontier behind
|
||||
// it, pinned: getter/setter JSDoc union returns (`cmd.name()` is typed
|
||||
// string | Command inside knownBy's body) and implicit-any FIELD writes of
|
||||
// class instances (`cmd.parent = this` — parent inferred `any` from its
|
||||
// constructor null, and a Command cannot ride the checked-dynamic slot).
|
||||
// it, pinned: safe package-maintained overload groups refine calls such as
|
||||
// `cmd.name()`, while implicit-any FIELD writes of class instances remain
|
||||
// (`cmd.parent = this` — parent inferred `any` from its constructor null,
|
||||
// and a Command cannot ride the checked-dynamic slot).
|
||||
import { Command } from "commander";
|
||||
const program = new Command();
|
||||
program.name("calc");
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
// Chained getter/setter calls typed by the package's own .d.ts overloads:
|
||||
// the auto opt-in drops chainy back to the island (its INFERRED surface
|
||||
// types name() as `string | Chainy`, breaking these chains) — the program
|
||||
// must stay analyzable either way.
|
||||
// Chained getter/setter calls typed by the package's own .d.ts overloads.
|
||||
// --npm-static projects the safe overload groups onto the matching runtime
|
||||
// JavaScript class, preserving the authored call surface while compiling
|
||||
// and validating the actual implementation body.
|
||||
import { Chainy } from "chainy";
|
||||
|
||||
const c = new Chainy();
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
// The commander shape in miniature: a getter/setter method whose declared
|
||||
// OVERLOADS (index.d.ts) say `name(): string` / `name(v): this`, while the
|
||||
// JS body infers the union `string | Chainy` — chained program-side calls
|
||||
// typecheck against the .d.ts and FAIL against the inferred surface.
|
||||
// JS body infers the union `string | Chainy`. --npm-static projects these
|
||||
// safe overload groups onto this exported runtime class before TS 7 parses
|
||||
// it, preserving precise calls while compiling the body below.
|
||||
'use strict';
|
||||
|
||||
class Chainy {
|
||||
|
||||
@@ -183,10 +183,10 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
// differential on the island lane (npm.test.ts) for now. Implicit-any
|
||||
// monomorphization moved the driven frontier BEHIND the typed-value →
|
||||
// untyped-param boundary (methods like _registerCommand and local
|
||||
// helpers like knownBy now instantiate per argument types); the next
|
||||
// fences are implicit-any FIELD writes of class instances (`cmd.parent
|
||||
// = this` — the field inferred `any` from its ctor null) and
|
||||
// getter/setter JSDoc union returns (`cmd.name()` → string | Command).
|
||||
// helpers like knownBy now instantiate per argument types). Safe
|
||||
// package-maintained overload groups preserve getter/setter results such
|
||||
// as `cmd.name()` → string; the next fences include implicit-any FIELD
|
||||
// writes of class instances (`cmd.parent = this`) and wider unknown flows.
|
||||
test("commander compiles static at the pinned coverage floor", () => {
|
||||
const { coverage } = analyze(join(fixturesRoot, "commander-calc/calc-npm-static.ts"), {
|
||||
npmStatic: ["commander"],
|
||||
@@ -198,6 +198,8 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
const failed = coverage.stats.statementsFailed + (coverage.unreached?.stats.statementsFailed ?? 0);
|
||||
expect(total).toBeGreaterThan(1000); // the whole package joined the program
|
||||
expect((total - failed) / total).toBeGreaterThanOrEqual(0.85);
|
||||
expect(total - failed).toBeGreaterThanOrEqual(1062);
|
||||
expect(coverage.runtimeFences?.length ?? 0).toBeLessThanOrEqual(115);
|
||||
}, 180_000);
|
||||
|
||||
// Tier 3: the island fallback — esbundled's chunk requires "net", an
|
||||
@@ -216,24 +218,26 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
expect(coverage.preflightFailed).toBe(false);
|
||||
}, 120_000);
|
||||
|
||||
// AUTO drops a package whose opt-in breaks the PROGRAM's own typecheck
|
||||
// (the .d.ts-overload vs inferred-surface gap — commander's chaining
|
||||
// shape in miniature): chainy's declared `name(): string / name(v):
|
||||
// this` overloads admit the chained spelling, its inferred surface
|
||||
// (`string | Chainy`) does not, so auto answers fallback with the
|
||||
// inferred-surface note and the program stays analyzable. Explicit
|
||||
// opt-ins keep the errors (the user asked for exactly that package).
|
||||
test("auto falls back when the program fails against an inferred surface", () => {
|
||||
const { coverage } = analyze(join(pilotRoot, "chainy-cli.ts"), { npmStatic: "auto" });
|
||||
expect(coverage.npmStatic).toEqual([
|
||||
{
|
||||
package: "chainy",
|
||||
status: "fallback",
|
||||
detail: "auto: the program does not typecheck against its inferred surface",
|
||||
},
|
||||
]);
|
||||
// A package-maintained .d.ts may state overloads its readable JS body
|
||||
// cannot infer. chainy is commander's getter/setter shape in miniature:
|
||||
// name()/tag() return strings with no argument and `this` with one. The
|
||||
// safe declaration groups project into JSDoc on the matching runtime
|
||||
// class, so the body still compiles from JS while calls keep the authored
|
||||
// surface — no island fallback and no unchecked declaration-only value.
|
||||
test("auto preserves safe declaration overloads while compiling the JavaScript body", async () => {
|
||||
const entry = join(pilotRoot, "chainy-cli.ts");
|
||||
const { coverage } = analyze(entry, { npmStatic: "auto" });
|
||||
expect(coverage.npmStatic).toEqual([{ package: "chainy", status: "static" }]);
|
||||
expect(coverage.preflightFailed).toBe(false);
|
||||
}, 120_000);
|
||||
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);
|
||||
}, 180_000);
|
||||
|
||||
// Non-opted UNTYPED node_modules packages keep the checked-dynamic
|
||||
// surface: maxNodeModuleJsDepth (active on every --npm-static load)
|
||||
|
||||
Reference in New Issue
Block a user