mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Merge main
This commit is contained in:
@@ -2,7 +2,7 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, own } from "../lowerer.js";
|
||||
import { lowerCheckedPredicateValue } from "../lower-builtin-values.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { BOOL, F64, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
|
||||
import { BOOL, F64, STRING, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
|
||||
import { lowerBuiltinValuePreservingUndefined, lowerOptionalNumberPredicate } from "./arguments.js";
|
||||
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
import { lowerStaticNumberGlobal } from "../lower-calls.js";
|
||||
@@ -88,6 +88,22 @@ export function lowerNumberStaticCall(
|
||||
const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments, loc);
|
||||
if (parsed !== null) return parsed;
|
||||
}
|
||||
// A checked-dynamic argument (`Number.parseInt(env.FORCE_COLOR, 10)`
|
||||
// over a JavaScript dictionary read): the parsers' own ToString,
|
||||
// exactly the global parseFloat's dyn form.
|
||||
if (probed?.type.kind === "dyn") {
|
||||
const text: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toStringCoerce",
|
||||
args: [probed],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
if (member === "parseFloat")
|
||||
return { kind: "libCall", fn: "num.parseFloat", args: [text], type: F64, loc };
|
||||
const radix = lowerer.lowerExprExpecting(call.arguments[1]!, F64);
|
||||
return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc };
|
||||
}
|
||||
const argText =
|
||||
probed !== null
|
||||
? lowerer.fmt(probed.type)
|
||||
|
||||
@@ -233,6 +233,41 @@ export function lowerProcessStreamProperty(
|
||||
* read in source order). Method members referenced without a call are
|
||||
* rejected specifically. Null for non-process receivers (the chain keeps
|
||||
* trying other property lowerings). */
|
||||
/** The value of a DATA member of the process global taken off by
|
||||
* destructuring (`const { env, argv } = process` in JavaScript, the
|
||||
* supports-color opening): the same values the spelled `process.env` /
|
||||
* `process.argv` reads produce, typed by the binding. Null for members
|
||||
* this does not cover (methods keep the destructuring fence). */
|
||||
export function processDataMemberValue(
|
||||
lowerer: Lowerer,
|
||||
member: string,
|
||||
binding: ts.Node,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
switch (member) {
|
||||
case "env": {
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(binding));
|
||||
if (mapped?.kind !== "record") return null;
|
||||
const helper = envSnapshotHelper(lowerer, mapped.shapeId, loc);
|
||||
return helper === null ? null : { kind: "call", callee: helper, args: [], type: mapped, loc };
|
||||
}
|
||||
case "argv":
|
||||
return { kind: "libCall", fn: "process.argv", args: [], type: arrayOf(STRING), loc };
|
||||
case "platform":
|
||||
return { kind: "libCall", fn: "process.platform", args: [], type: STRING, loc };
|
||||
case "arch":
|
||||
return { kind: "libCall", fn: "process.arch", args: [], type: STRING, loc };
|
||||
case "pid":
|
||||
return { kind: "libCall", fn: "process.pid", args: [], type: F64, loc };
|
||||
case "execPath":
|
||||
return { kind: "libCall", fn: "process.execPath", args: [], type: STRING, loc };
|
||||
case "versions":
|
||||
return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function lowerProcessProperty(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
|
||||
@@ -1159,7 +1159,9 @@ export class ClassDynamicDispatch {
|
||||
// Runtime-owned roots keep their dedicated property contracts. A
|
||||
// family whose fields all exist from construction and are never
|
||||
// deleted keeps a fixed layout, as TypeScript classes do.
|
||||
this.deletedNames ??= deletedPropertyNames(lowerer.moduleOrder);
|
||||
// null is a result too; undefined means not computed yet.
|
||||
if (this.deletedNames === undefined)
|
||||
this.deletedNames = deletedPropertyNames(lowerer.moduleOrder);
|
||||
if (!root.base?.def.runtime && !fixedFieldFamily(root, this.deletedNames)) track(root);
|
||||
}
|
||||
const loc = this.propertyBag.loc;
|
||||
|
||||
@@ -124,15 +124,6 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca
|
||||
continue;
|
||||
}
|
||||
if (ts.isExpressionStatement(stmt) && !exposes(stmt.expression, depth)) continue;
|
||||
// A check that only reads assigned fields and throws or does nothing
|
||||
// with the receiver (`if (this.size < 0) throw new RangeError()`)
|
||||
// does not expose the instance; assignments in its branches are not
|
||||
// definite and still count as exposures.
|
||||
if (
|
||||
(ts.isIfStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBlock(stmt)) &&
|
||||
!exposes(stmt, depth)
|
||||
)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { closeSourceParser, parseSourceFile } from "../ts7/source-parser-node.js";
|
||||
import { isValueReturningConstructorClass } from "./js-dynamic-objects.js";
|
||||
|
||||
afterAll(closeSourceParser);
|
||||
|
||||
function classify(source: string): boolean {
|
||||
const file = parseSourceFile("classes.js", source, "js");
|
||||
let decl: ts.ClassDeclaration | undefined;
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (!decl && ts.isClassDeclaration(node)) decl = node;
|
||||
});
|
||||
if (!decl) throw new Error("fixture has no class");
|
||||
return isValueReturningConstructorClass(decl);
|
||||
}
|
||||
|
||||
test("a constructor-only class returning a value is claimed", () => {
|
||||
expect(classify(`class Chalk { constructor(options) { return chalkFactory(options); } }`)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(classify(`class C { constructor(a) { if (a) return { a }; return [a]; } ; }`)).toBe(true);
|
||||
expect(classify(`class C { constructor() { const f = () => 1; return { f }; } }`)).toBe(true);
|
||||
});
|
||||
|
||||
test("classes that keep their instance layout are left alone", () => {
|
||||
// No value return, or only nested functions return.
|
||||
expect(classify(`class C { constructor() { return; } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { function f() { return 1; } f(); } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { const f = () => { return 1; }; f(); } }`)).toBe(false);
|
||||
// Other members, heritage, or a constructor that observes `this`.
|
||||
expect(classify(`class C { constructor() { return {}; } m() {} }`)).toBe(false);
|
||||
expect(classify(`class C { x = 1; constructor() { return {}; } }`)).toBe(false);
|
||||
expect(classify(`class C extends B { constructor() { super(); return {}; } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { this.a = 1; return {}; } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { const g = () => this; return { g }; } }`)).toBe(false);
|
||||
expect(classify(`class C {}`)).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,312 @@
|
||||
/* JavaScript declarations whose values only the checked-dynamic
|
||||
* representation can hold faithfully. TypeScript infers a fixed object
|
||||
* type for each of them, but the program changes the value's shape or
|
||||
* identity at run time, so a static layout would answer stale members
|
||||
* (or throw on a value that no longer fits):
|
||||
*
|
||||
* - a non-empty object literal bound to a variable that the same file
|
||||
* grows or reshapes dynamically: a keyed write with a runtime key
|
||||
* (`o[k] = v`), a `delete`, or `Object.defineProperty(o, ...)` /
|
||||
* `Object.defineProperties(o, ...)` (ansi-styles' `assembleStyles`).
|
||||
* The object literals nested in its property values belong to the same
|
||||
* value graph and are marked with it;
|
||||
* - a class whose only member is a constructor that returns a value and
|
||||
* never reads `this` (chalk's `class Chalk { constructor(options) {
|
||||
* return chalkFactory(options); } }`): `new C()` answers whatever the
|
||||
* constructor returns, so its instance type has no fixed layout.
|
||||
*
|
||||
* The type mapper answers the checked-dynamic type for the instance and
|
||||
* object-literal types these declarations own (TypeMapperCtx
|
||||
* .jsDynamicDeclaration). The scan is syntactic, runs over JavaScript
|
||||
* files only, and an empty answer leaves every mapping unchanged, so
|
||||
* TypeScript programs and JavaScript that never uses these patterns
|
||||
* compile exactly as before. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { BOOL, DYN, type IrExpr, type IrStmt } from "../../ir/ir.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr } from "../../ir/build.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ParamShape } from "./call-signatures.js";
|
||||
import { newFnCtx } from "./function-context.js";
|
||||
|
||||
const SCANNED_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
||||
ts.SyntaxKind.VariableDeclaration,
|
||||
ts.SyntaxKind.ClassDeclaration,
|
||||
ts.SyntaxKind.BinaryExpression,
|
||||
ts.SyntaxKind.DeleteExpression,
|
||||
ts.SyntaxKind.CallExpression,
|
||||
]);
|
||||
|
||||
function unparenthesized(node: ts.Expression): ts.Expression {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
return node;
|
||||
}
|
||||
|
||||
function isLiteralKey(node: ts.Expression): boolean {
|
||||
const key = unparenthesized(node);
|
||||
return (
|
||||
ts.isStringLiteral(key) || ts.isNumericLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)
|
||||
);
|
||||
}
|
||||
|
||||
function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
|
||||
return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
|
||||
}
|
||||
|
||||
/** `class C { constructor(...) { ...; return value; } }` with nothing else:
|
||||
* no heritage, decorators or other members, a constructor body that
|
||||
* returns a value at its own level and never mentions `this`/`super`. */
|
||||
export function isValueReturningConstructorClass(decl: ts.ClassDeclaration): boolean {
|
||||
if (decl.heritageClauses !== undefined && decl.heritageClauses.length > 0) return false;
|
||||
if (decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.Decorator)) return false;
|
||||
let ctor: ts.ConstructorDeclaration | undefined;
|
||||
for (const member of decl.members) {
|
||||
if (ts.isSemicolonClassElement(member)) continue;
|
||||
if (!ts.isConstructorDeclaration(member) || ctor !== undefined) return false;
|
||||
ctor = member;
|
||||
}
|
||||
if (ctor?.body === undefined) return false;
|
||||
let returnsValue = false;
|
||||
let observesThis = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (observesThis) return;
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.SuperKeyword) {
|
||||
observesThis = true;
|
||||
return;
|
||||
}
|
||||
// Arrow functions share the constructor's `this`; other functions
|
||||
// have their own `this` and their own returns.
|
||||
if (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return;
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return;
|
||||
if (ts.isReturnStatement(node) && node.expression !== undefined && !insideArrow(node, ctor!))
|
||||
returnsValue = true;
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
ctor.body.forEachChild(visit);
|
||||
for (const parameter of ctor.parameters) parameter.forEachChild(visit);
|
||||
return returnsValue && !observesThis;
|
||||
}
|
||||
|
||||
function insideArrow(node: ts.Node, ctor: ts.ConstructorDeclaration): boolean {
|
||||
for (let p = node.parent; p !== undefined && p !== ctor; p = p.parent) {
|
||||
if (ts.isArrowFunction(p)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function markNested(literal: ts.ObjectLiteralExpression, out: Set<ts.Node>): void {
|
||||
if (out.has(literal)) return;
|
||||
out.add(literal);
|
||||
for (const property of literal.properties) {
|
||||
if (!ts.isPropertyAssignment(property)) continue;
|
||||
const value = unparenthesized(property.initializer);
|
||||
if (ts.isObjectLiteralExpression(value)) markNested(value, out);
|
||||
}
|
||||
}
|
||||
|
||||
export function jsDynamicDeclarations(
|
||||
files: readonly ts.SourceFile[],
|
||||
checker: ts.TypeChecker,
|
||||
): Set<ts.Node> {
|
||||
const out = new Set<ts.Node>();
|
||||
for (const file of files) {
|
||||
if (!isJsSourceFile(file) || file.isDeclarationFile) continue;
|
||||
const literals = new Map<ts.Symbol, ts.ObjectLiteralExpression>();
|
||||
const reshaped = new Set<ts.Symbol>();
|
||||
const receiver = (expression: ts.Expression | undefined): void => {
|
||||
if (expression === undefined) return;
|
||||
const target = unparenthesized(expression);
|
||||
if (!ts.isIdentifier(target)) return;
|
||||
const symbol = checker.getSymbolAtLocation(target);
|
||||
if (symbol !== undefined) reshaped.add(symbol);
|
||||
};
|
||||
ts.walkPreorder(
|
||||
file,
|
||||
(node) => {
|
||||
if (ts.isVariableDeclaration(node)) {
|
||||
if (!ts.isIdentifier(node.name) || node.initializer === undefined) return;
|
||||
const init = unparenthesized(node.initializer);
|
||||
// An empty literal's binding is already checked-dynamic
|
||||
// (jsOpenObjectType), and its type describes no layout.
|
||||
if (!ts.isObjectLiteralExpression(init) || init.properties.length === 0) return;
|
||||
const symbol = checker.getSymbolAtLocation(node.name);
|
||||
if (symbol !== undefined) literals.set(symbol, init);
|
||||
return;
|
||||
}
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
if (isValueReturningConstructorClass(node)) out.add(node);
|
||||
return;
|
||||
}
|
||||
if (ts.isBinaryExpression(node)) {
|
||||
if (!isAssignmentOperator(node.operatorToken.kind)) return;
|
||||
const left = unparenthesized(node.left);
|
||||
if (ts.isElementAccessExpression(left) && !isLiteralKey(left.argumentExpression))
|
||||
receiver(left.expression);
|
||||
return;
|
||||
}
|
||||
if (ts.isDeleteExpression(node)) {
|
||||
const operand = unparenthesized(node.expression);
|
||||
if (ts.isElementAccessExpression(operand) || ts.isPropertyAccessExpression(operand))
|
||||
receiver(operand.expression);
|
||||
return;
|
||||
}
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = unparenthesized(node.expression);
|
||||
if (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
ts.isIdentifier(callee.expression) &&
|
||||
callee.expression.text === "Object" &&
|
||||
(callee.name.text === "defineProperty" || callee.name.text === "defineProperties")
|
||||
)
|
||||
receiver(node.arguments[0]);
|
||||
}
|
||||
},
|
||||
SCANNED_KINDS,
|
||||
);
|
||||
for (const symbol of reshaped) {
|
||||
const literal = literals.get(symbol);
|
||||
if (literal !== undefined) markNested(literal, out);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `new C(args)` for a value-returning constructor class (above): the
|
||||
* constructor body runs as an ordinary JavaScript function whose result
|
||||
* is the construction's value. A returned object or function is Node's
|
||||
* answer; a fall-through or primitive return would answer the bare
|
||||
* instance, which has no static home here, and throws the deferred
|
||||
* refusal instead. Null when the callee is not such a class. */
|
||||
export function lowerValueReturningConstructorNew(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.NewExpression,
|
||||
): IrExpr | null {
|
||||
const isDynamic = lowerer.typeCtx.jsDynamicDeclaration;
|
||||
if (isDynamic === undefined) return null;
|
||||
const callee = unparenthesized(expr.expression);
|
||||
if (!ts.isIdentifier(callee)) return null;
|
||||
let symbol = lowerer.resolveValueSymbol(callee);
|
||||
if (symbol !== null && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const decl = symbol
|
||||
? lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.find((node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node))
|
||||
: undefined;
|
||||
if (decl === undefined) return null;
|
||||
const loc = locOf(expr);
|
||||
const ctor = decl.members.find(ts.isConstructorDeclaration)!;
|
||||
const simple = ctor.parameters.every(
|
||||
(p) => ts.isIdentifier(p.name) && p.dotDotDotToken === undefined && p.initializer === undefined,
|
||||
);
|
||||
if (!simple || !ts.isSourceFile(decl.parent)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"constructing a class whose constructor returns a value, with parameter defaults, rest or patterns, or declared inside a function",
|
||||
);
|
||||
}
|
||||
const args = expr.arguments ?? [];
|
||||
if (args.some(ts.isSpreadElement)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"spread arguments when constructing a class whose constructor returns a value",
|
||||
);
|
||||
}
|
||||
const key = `ctorValue:${decl.getSourceFile().fileName}:${decl.getStart()}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (name === undefined) {
|
||||
name = `%ctorValue.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const savedClass = lowerer.currentClass;
|
||||
lowerer.currentClass = null;
|
||||
lowerer.fnStack.push(newFnCtx(false, null, null, DYN));
|
||||
try {
|
||||
const shapes: ParamShape[] = ctor.parameters.map(() => ({ type: DYN, mode: "required" }));
|
||||
const declared = lowerer.declareParams(ctor.parameters, shapes);
|
||||
const body: IrStmt[] = [
|
||||
...declared.prologue,
|
||||
...lowerer.lowerStmts(ctor.body!.statements),
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: "SC1090",
|
||||
message: `constructors that return no object (the bare instance of '${decl.name?.text ?? "class"}' has no representation here) are not supported yet [SC1090 at ${decl.getSourceFile().fileName}:${ts.getLineAndCharacterOfPosition(decl.getSourceFile(), ctor.getStart()).line + 1}]`,
|
||||
loc: locOf(ctor),
|
||||
},
|
||||
];
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: declared.params,
|
||||
returnType: DYN,
|
||||
locals: lowerer.ctx.locals,
|
||||
body,
|
||||
loc: locOf(ctor),
|
||||
});
|
||||
} finally {
|
||||
lowerer.fnStack.pop();
|
||||
lowerer.currentClass = savedClass;
|
||||
}
|
||||
}
|
||||
const lowered = ctor.parameters.map((_, i) =>
|
||||
args[i] !== undefined ? lowerer.lowerExprExpecting(args[i]!, DYN) : dynUndefinedExpr(loc),
|
||||
);
|
||||
// Surplus arguments still evaluate, left to right, after the others.
|
||||
const surplus: IrStmt[] = args
|
||||
.slice(ctor.parameters.length)
|
||||
.map((arg) => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }));
|
||||
const result = lowerer.declareHiddenLocal("%constructed", DYN);
|
||||
const value: IrExpr = { kind: "varRef", localId: result.id, type: DYN, loc };
|
||||
const sf = expr.getSourceFile();
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, expr.getStart()).line + 1;
|
||||
const objectLike: IrExpr = {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: { kind: "dynTest", test: "function", value, type: BOOL, loc },
|
||||
right: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: { kind: "dynTest", test: "object", value, type: BOOL, loc },
|
||||
right: { kind: "dynTest", test: "null", negated: true, value, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const args0: IrStmt[] = [];
|
||||
const argLocals = lowered.map((arg) => {
|
||||
const local = lowerer.declareHiddenLocal("%constructArg", DYN);
|
||||
args0.push({ kind: "varDecl", localId: local.id, init: arg, loc });
|
||||
return { kind: "varRef", localId: local.id, type: DYN, loc } as IrExpr;
|
||||
});
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
...args0,
|
||||
...surplus,
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: result.id,
|
||||
init: { kind: "call", callee: name, args: argLocals, type: DYN, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: objectLike,
|
||||
then: value,
|
||||
else_: nodeThrowExpr(
|
||||
0,
|
||||
"SC1090",
|
||||
`constructors that return no object are not supported yet [SC1090 at ${sf.fileName}:${line}]`,
|
||||
DYN,
|
||||
loc,
|
||||
),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -3480,13 +3480,41 @@ function lowerDynReceiverMethodCall(
|
||||
// its name overlaps a primitive prototype (`colors.bold(...)`): the
|
||||
// source value was compiled from that object shape, while genuinely
|
||||
// untyped/any receivers retain the runtime-kind ambiguity and fence.
|
||||
if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable) return null;
|
||||
// A name only String.prototype declares (`bold`, `italics`, `link`,
|
||||
// ...) on a checker-untyped receiver: every other dyn kind answers its
|
||||
// own or inherited member exactly like a non-prototype name (chalk's
|
||||
// `chalk.red.bold(...)` reads a prototype getter of a function), so
|
||||
// only a string receiver keeps the refusal, deferred to run time.
|
||||
const stringOnlyName =
|
||||
checkerUntyped && !ownCallable && DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text);
|
||||
if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable && !stringOnlyName) return null;
|
||||
// Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain
|
||||
// machinery's short-circuit semantics — not modeled here yet.
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
const loc = locOf(call);
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN);
|
||||
const receiver: IrExpr = { kind: "varRef", localId: receiverLocal.id, type: DYN, loc };
|
||||
// The string refusal throws in place of the member read (a seqExpr
|
||||
// statement cannot leave the expression).
|
||||
const stringRefusal = (read: IrExpr): IrExpr => {
|
||||
if (!stringOnlyName) return read;
|
||||
const sf = call.getSourceFile();
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, call.getStart(sf)).line + 1;
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc },
|
||||
then: nodeThrowExpr(
|
||||
0,
|
||||
"SC1090",
|
||||
`method calls like '${access.getText()}' on a string are not supported yet [SC1090 at ${sf.fileName}:${line}]`,
|
||||
DYN,
|
||||
loc,
|
||||
),
|
||||
else_: read,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
};
|
||||
const member: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
key: { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access) },
|
||||
@@ -3498,7 +3526,7 @@ function lowerDynReceiverMethodCall(
|
||||
const callee = varRef(calleeLocal.id, DYN, loc);
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc },
|
||||
{ kind: "varDecl", localId: calleeLocal.id, init: member, loc },
|
||||
{ kind: "varDecl", localId: calleeLocal.id, init: stringRefusal(member), loc },
|
||||
];
|
||||
if (call.arguments.some(ts.isSpreadElement)) {
|
||||
const spread = lowerSpreadArgsCall(lowerer, call, callee, loc);
|
||||
@@ -3607,6 +3635,31 @@ const DYN_PROTO_METHOD_NAMES = new Set([
|
||||
"toPrecision",
|
||||
]);
|
||||
|
||||
/** The DYN_PROTO_METHOD_NAMES that only String.prototype declares among
|
||||
* the dyn-representable kinds' prototypes. */
|
||||
const DYN_STRING_PROTO_ONLY_NAMES = new Set([
|
||||
"anchor",
|
||||
"big",
|
||||
"blink",
|
||||
"bold",
|
||||
"codePointAt",
|
||||
"fixed",
|
||||
"fontcolor",
|
||||
"fontsize",
|
||||
"isWellFormed",
|
||||
"italics",
|
||||
"link",
|
||||
"localeCompare",
|
||||
"normalize",
|
||||
"small",
|
||||
"strike",
|
||||
"sub",
|
||||
"sup",
|
||||
"toLocaleLowerCase",
|
||||
"toLocaleUpperCase",
|
||||
"toWellFormed",
|
||||
]);
|
||||
|
||||
/** The SHARED prototype names scr_dyn_invoke dispatches at runtime (the
|
||||
* subset of DYN_PROTO_METHOD_NAMES with a receiver-kind dispatch): the
|
||||
* runtime runs the real method for the receiver's kind, throws Node's
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { checkedIterableSpread } from "./checked-iterable-spread.js";
|
||||
import { lowerReadlineUiNew } from "./builtins/readline.js";
|
||||
@@ -143,7 +144,7 @@ import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
import { constructionEscape, mentionsReceiver } from "./construction-escapes.js";
|
||||
import { constructionEscape } from "./construction-escapes.js";
|
||||
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
|
||||
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
import { reflectedClassStaticMethodValue } from "./class-method-values.js";
|
||||
@@ -468,11 +469,6 @@ export interface ClassInfo {
|
||||
* unassigned read throws the catchable TypeError instead of yielding
|
||||
* an undefined the declared type cannot hold (SEMANTICS.md). */
|
||||
deferredInitFields?: Set<string>;
|
||||
/** Fields (inherited included) that code construction exposes the
|
||||
* instance to can read before their assignment: the slot is the
|
||||
* undefined-armed union, allocation writes undefined, and reads yield the
|
||||
* union value, so an early read sees undefined as in Node. */
|
||||
earlyReadFields?: Set<string>;
|
||||
/** True when construction may hand the instance to other code (a method
|
||||
* call, an argument, a callback, or a base constructor that does) — a
|
||||
* subclass's own fields can then be read before their assignment. */
|
||||
@@ -3507,54 +3503,55 @@ export function collectClassShapeInner(
|
||||
const derived =
|
||||
decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ??
|
||||
false;
|
||||
// A generic family is not emitted; its instances and each mixin
|
||||
// application analyze their own construction with their actual base.
|
||||
const escape: { escapes: boolean; assigned: Set<string> | null } = familyMode
|
||||
? { escapes: derived, assigned: null }
|
||||
: constructionEscape({
|
||||
ctor,
|
||||
fields: fieldOrder.flatMap((f) =>
|
||||
paramProps.some((pp) => pp.name === f.name)
|
||||
? []
|
||||
: [{ name: f.name, initializer: f.initializer }],
|
||||
),
|
||||
paramProps: paramProps.map((pp) => pp.name),
|
||||
derived,
|
||||
baseEscapes:
|
||||
base === null ||
|
||||
callableBase !== undefined ||
|
||||
factoryBaseExpression !== undefined ||
|
||||
base.constructionEscapes === true,
|
||||
isField: (name) =>
|
||||
fields.has(name) &&
|
||||
!methods.has(`set:${name}`) &&
|
||||
!lowerer.findMethodOn(base, `set:${name}`),
|
||||
privateMethodBody: (call) => {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee)) return null;
|
||||
const name = callee.name.text;
|
||||
const declaration = decl.members.find(
|
||||
(member): member is ts.MethodDeclaration =>
|
||||
ts.isMethodDeclaration(member) &&
|
||||
member.body !== undefined &&
|
||||
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
||||
member.name.text === name,
|
||||
);
|
||||
if (!declaration?.body || declaration.asteriskToken) return null;
|
||||
const modifiers = ts.getModifiers(declaration) ?? [];
|
||||
if (
|
||||
modifiers.some(
|
||||
(m) =>
|
||||
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
|
||||
// Generic instances and mixins share layouts across declarations, so
|
||||
// they only report whether construction exposes the instance.
|
||||
const escape: { escapes: boolean; assigned: Set<string> | null } =
|
||||
familyMode || mixin
|
||||
? { escapes: derived, assigned: null }
|
||||
: constructionEscape({
|
||||
ctor,
|
||||
fields: fieldOrder.flatMap((f) =>
|
||||
paramProps.some((pp) => pp.name === f.name)
|
||||
? []
|
||||
: [{ name: f.name, initializer: f.initializer }],
|
||||
),
|
||||
paramProps: paramProps.map((pp) => pp.name),
|
||||
derived,
|
||||
baseEscapes:
|
||||
base === null ||
|
||||
callableBase !== undefined ||
|
||||
factoryBaseExpression !== undefined ||
|
||||
base.constructionEscapes === true,
|
||||
isField: (name) =>
|
||||
fields.has(name) &&
|
||||
!methods.has(`set:${name}`) &&
|
||||
!lowerer.findMethodOn(base, `set:${name}`),
|
||||
privateMethodBody: (call) => {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee)) return null;
|
||||
const name = callee.name.text;
|
||||
const declaration = decl.members.find(
|
||||
(member): member is ts.MethodDeclaration =>
|
||||
ts.isMethodDeclaration(member) &&
|
||||
member.body !== undefined &&
|
||||
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
||||
member.name.text === name,
|
||||
);
|
||||
if (!declaration?.body || declaration.asteriskToken) return null;
|
||||
const modifiers = ts.getModifiers(declaration) ?? [];
|
||||
if (
|
||||
modifiers.some(
|
||||
(m) =>
|
||||
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
|
||||
)
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const isPrivate =
|
||||
ts.isPrivateIdentifier(declaration.name) ||
|
||||
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
|
||||
return isPrivate ? declaration.body : null;
|
||||
},
|
||||
});
|
||||
return null;
|
||||
const isPrivate =
|
||||
ts.isPrivateIdentifier(declaration.name) ||
|
||||
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
|
||||
return isPrivate ? declaration.body : null;
|
||||
},
|
||||
});
|
||||
// The deferred definite-assignment check: a field on the unguarded
|
||||
// list passes only with an unconditional `this.x = ...` at the
|
||||
// constructor's TOP LEVEL — the same standard the JS-class path
|
||||
@@ -3610,30 +3607,18 @@ export function collectClassShapeInner(
|
||||
}
|
||||
}
|
||||
|
||||
// Fields assigned after construction can expose the instance (a method
|
||||
// call, a base constructor that calls an override) can be read while
|
||||
// Node still holds undefined. Their slots admit undefined, allocation
|
||||
// writes it, and reads yield it, as in Node.
|
||||
const earlyReadFields = new Set<string>(base?.earlyReadFields ?? []);
|
||||
if (escape.assigned && escape.escapes) {
|
||||
if (escape.assigned && !inst) {
|
||||
for (const f of fieldOrder) {
|
||||
if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue;
|
||||
if (paramProps.some((pp) => pp.name === f.name)) continue;
|
||||
const declared = fields.get(f.name);
|
||||
if (declared === undefined || !earlyReadable(lowerer, declared)) continue;
|
||||
// A declared `T | undefined` field already starts undefined.
|
||||
if (declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0)
|
||||
if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime)
|
||||
continue;
|
||||
const armed = lowerer.runtimeOptionalType(declared);
|
||||
if (armed.kind !== "union") continue;
|
||||
const armed = lowerer.withUndefinedArm(declared);
|
||||
if (armed === null || armed.kind !== "union") continue;
|
||||
fields.set(f.name, armed);
|
||||
f.type = armed;
|
||||
// Primitive and union slots yield the undefined value itself.
|
||||
// Reference slots keep the deferred-init reads: consumers that
|
||||
// observe undefined (printing, equality, typeof, nullish defaults)
|
||||
// see it, and member access on it throws a TypeError, as in Node.
|
||||
if (declared.kind === "union" || PRIMITIVE_EARLY_READS.has(declared.kind))
|
||||
earlyReadFields.add(f.name);
|
||||
else deferredInitFields.add(f.name);
|
||||
deferredInitFields.add(f.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3738,43 +3723,6 @@ export function collectClassShapeInner(
|
||||
(name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name),
|
||||
)
|
||||
: null;
|
||||
// Constructor statements that run after the instance may have been
|
||||
// exposed (a method call, `this` passed along, a base constructor
|
||||
// that exposes it): a field they create can be read before its
|
||||
// assignment, while Node holds undefined.
|
||||
const exposedBefore = new Set<ts.Statement>();
|
||||
{
|
||||
let exposed = false;
|
||||
for (const stmt of ctor?.body?.statements ?? []) {
|
||||
const expression = ts.isExpressionStatement(stmt) ? stmt.expression : null;
|
||||
if (
|
||||
expression &&
|
||||
ts.isCallExpression(expression) &&
|
||||
expression.expression.kind === ts.SyntaxKind.SuperKeyword
|
||||
) {
|
||||
if (base?.constructionEscapes === true || expression.arguments.some(mentionsReceiver))
|
||||
exposed = true;
|
||||
continue;
|
||||
}
|
||||
const assigned =
|
||||
expression &&
|
||||
ts.isBinaryExpression(expression) &&
|
||||
expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(expression.left) &&
|
||||
expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
? expression
|
||||
: null;
|
||||
if (exposed || (assigned && mentionsReceiver(assigned.right))) exposedBefore.add(stmt);
|
||||
const plainAssignment =
|
||||
assigned !== null &&
|
||||
ts.isPropertyAccessExpression(assigned.left) &&
|
||||
!mentionsReceiver(assigned.right) &&
|
||||
!hasAccessor(assigned.left.name.text) &&
|
||||
!methods.has(assigned.left.name.text) &&
|
||||
!lowerer.findMethodOn(base, assigned.left.name.text);
|
||||
if (!plainAssignment && mentionsReceiver(stmt)) exposed = true;
|
||||
}
|
||||
}
|
||||
if (ctor?.body) {
|
||||
for (const stmt of ctor.body.statements) {
|
||||
const lhs =
|
||||
@@ -3845,15 +3793,8 @@ export function collectClassShapeInner(
|
||||
type = inheritedJsFieldType(lowerer, decl, name, type);
|
||||
// super() can invoke a subclass setter or method before its
|
||||
// constructor assignments run. These slots start undefined.
|
||||
const sealed = sealedPrefix?.created.has(name) === true;
|
||||
if (base && lowerer.dynConvertible(type) && !sealed) type = DYN;
|
||||
if (exposedBefore.has(stmt) && !sealed && earlyReadable(lowerer, type)) {
|
||||
const armed = lowerer.runtimeOptionalType(type);
|
||||
if (armed.kind === "union") {
|
||||
type = armed;
|
||||
earlyReadFields.add(name);
|
||||
}
|
||||
}
|
||||
if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name))
|
||||
type = DYN;
|
||||
fields.set(name, type);
|
||||
fieldOrder.push({ name, type, initializer: undefined });
|
||||
continue;
|
||||
@@ -4114,7 +4055,6 @@ export function collectClassShapeInner(
|
||||
? { classDecorators: { nodes: classDecoratorNodes } }
|
||||
: {}),
|
||||
...(deferredInitFields.size > 0 ? { deferredInitFields } : {}),
|
||||
...(earlyReadFields.size > 0 ? { earlyReadFields } : {}),
|
||||
...(escape.escapes ? { constructionEscapes: true as const } : {}),
|
||||
};
|
||||
// GENERIC members get their declaring-class backlink now that the
|
||||
@@ -6289,33 +6229,6 @@ function overrideReturnAbi(
|
||||
return declared;
|
||||
}
|
||||
|
||||
/** Field kinds whose early reads yield the undefined value (earlyReadFields). */
|
||||
const PRIMITIVE_EARLY_READS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"f64",
|
||||
"bool",
|
||||
"string",
|
||||
"bigint",
|
||||
]);
|
||||
|
||||
/** Field types whose slot can admit undefined for reads before the field's
|
||||
* assignment. Date values are scalars, so their unassigned slot is memory
|
||||
* safe; checked-dynamic slots already hold undefined. */
|
||||
function earlyReadable(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (isUnitType(type)) return false;
|
||||
switch (type.kind) {
|
||||
case "dyn":
|
||||
case "void":
|
||||
case "date":
|
||||
case "generator":
|
||||
case "caught":
|
||||
return false;
|
||||
case "object":
|
||||
return lowerer.classes.get(type.className)?.def.runtime !== true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** The nearest declaration of `name` at or above `info` — the method a
|
||||
* receiver of that static class runs when nothing below overrides it. */
|
||||
export function findMethodOn(
|
||||
@@ -7753,11 +7666,7 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca
|
||||
obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc },
|
||||
className: info.def.name,
|
||||
field: pp.name,
|
||||
// An early-readable field's slot admits undefined (earlyReadFields).
|
||||
value: lowerer.coerceToExpected(
|
||||
{ kind: "varRef", localId: local!.id, type: local!.type, loc },
|
||||
info.fields.get(pp.name) ?? local!.type,
|
||||
),
|
||||
value: { kind: "varRef", localId: local!.id, type: local!.type, loc },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
@@ -8551,6 +8460,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
undefined,
|
||||
lowerer.checker.getSymbolAtLocation(expr.expression),
|
||||
);
|
||||
const valueReturning = lowerValueReturningConstructorNew(lowerer, expr);
|
||||
if (valueReturning) return valueReturning;
|
||||
const instanceConstructor = lowerInstanceConstructorNew(lowerer, expr);
|
||||
if (instanceConstructor) return instanceConstructor;
|
||||
const objectFactory = lowerObjectFactoryNew(lowerer, expr);
|
||||
|
||||
@@ -3161,51 +3161,6 @@ export function lowerTernary(
|
||||
* the same union (unnarrowed use), to a SUB-union (partial narrowing —
|
||||
* unrepresentable without a re-tag), or to nothing (`never` in an
|
||||
* exhaustive default) leaves the expression union-typed. */
|
||||
/** Uses that read an early-read field's value as Node does when it is
|
||||
* undefined: printing, templates, operators, tests, member access, calls of
|
||||
* program functions (whose parameters the optional-value analysis widens),
|
||||
* and stores into bindings, fields and returns. Other uses, such as library
|
||||
* arguments, extract the declared type with a check. */
|
||||
function observesEarlyRead(node: ts.Node): boolean {
|
||||
let child = node;
|
||||
let parent = node.parent;
|
||||
while (parent && ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (!parent) return false;
|
||||
if (
|
||||
ts.isTemplateSpan(parent) ||
|
||||
ts.isBinaryExpression(parent) ||
|
||||
ts.isPrefixUnaryExpression(parent) ||
|
||||
ts.isPostfixUnaryExpression(parent) ||
|
||||
ts.isTypeOfExpression(parent) ||
|
||||
ts.isConditionalExpression(parent) ||
|
||||
ts.isIfStatement(parent) ||
|
||||
ts.isWhileStatement(parent) ||
|
||||
ts.isReturnStatement(parent) ||
|
||||
ts.isVariableDeclaration(parent) ||
|
||||
ts.isPropertyAssignment(parent) ||
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
(ts.isPropertyAccessExpression(parent) && parent.expression === child) ||
|
||||
(ts.isElementAccessExpression(parent) && parent.expression === child)
|
||||
)
|
||||
return true;
|
||||
if (ts.isCallExpression(parent) && parent.arguments.includes(child as ts.Expression)) {
|
||||
const callee = parent.expression;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
ts.isIdentifier(callee.expression) &&
|
||||
callee.expression.text === "console"
|
||||
)
|
||||
return true;
|
||||
return (
|
||||
!ts.isPropertyAccessExpression(callee) || callee.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr {
|
||||
if (
|
||||
(expr.type.kind === "union" || expr.type.kind === "dyn") &&
|
||||
@@ -3416,25 +3371,6 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
if (expr.type.kind !== "union") return expr;
|
||||
if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node))
|
||||
return expr;
|
||||
// A field read before its assignment holds undefined although its
|
||||
// declared type omits it; the declared type is no narrowing proof. A flow
|
||||
// narrowing past the declared type (`if (this.x !== null)`) extracts the
|
||||
// narrowed arm with a check, since the slot may still hold undefined.
|
||||
if (
|
||||
expr.kind === "fieldGet" &&
|
||||
lowerer.classes.get(expr.className)?.earlyReadFields?.has(expr.field) === true
|
||||
) {
|
||||
const symbol = ts.isPropertyAccessExpression(node)
|
||||
? lowerer.checker.getSymbolAtLocation(node.name)
|
||||
: undefined;
|
||||
const declared = symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null;
|
||||
const narrowedArm = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (narrowedArm === null || narrowedArm.kind === "union") return expr;
|
||||
if (declared !== null && typeEquals(declared, narrowedArm) && observesEarlyRead(node))
|
||||
return expr;
|
||||
const helper = lowerer.narrowedArmHelper(expr.type.unionId, narrowedArm, expr.loc);
|
||||
return helper ? lowerer.checkedNarrowCall(helper, expr, narrowedArm, expr.loc) : expr;
|
||||
}
|
||||
|
||||
// Represented fields and index values can include undefined even when
|
||||
// checker inference or lib.d.ts declares strings (RegExp captures).
|
||||
@@ -5082,6 +5018,34 @@ export function lowerIntrinsicProperty(
|
||||
return { kind: "libCall", fn: "dyn.arrayPrototype", args: [], type: DYN, loc: locOf(expr) };
|
||||
if (lowerer.stdlibGlobalMember(expr, "Object") === "prototype")
|
||||
return { kind: "libCall", fn: "dyn.objectPrototype", args: [], type: DYN, loc: locOf(expr) };
|
||||
// Function.prototype as a VALUE (a prototype-chain link:
|
||||
// `Object.setPrototypeOf(F.prototype, Function.prototype)`): one
|
||||
// anonymous function answering undefined, length 0 — a zero-capture
|
||||
// closure of a compiler helper, so every read is the same function
|
||||
// (closure identity) and shares its property table. Reads through it
|
||||
// find the function members the checked runtime models for every
|
||||
// function (apply, constructor, name, length). Member forms keep their
|
||||
// own lowerings.
|
||||
if (lowerer.stdlibGlobalMember(expr, "Function") === "prototype") {
|
||||
const loc = locOf(expr);
|
||||
const name = "%dyn.functionPrototype";
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name))
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: DYN,
|
||||
body: [{ kind: "return", value: dynUndefinedExpr(loc), loc }],
|
||||
loc,
|
||||
});
|
||||
return {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc },
|
||||
fnName: "",
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (lowerer.stdlibGlobalMember(expr, "Array") === "isArray")
|
||||
return lowerArrayIsArrayValue(lowerer, locOf(expr));
|
||||
if (lowerer.stdlibGlobalMember(expr, "Array") === "from")
|
||||
@@ -11538,6 +11502,34 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
|
||||
// same lift the union-operand path above takes, arriving here
|
||||
// with two plain arm values instead.
|
||||
const target = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
// An always-truthy left operand (a record, object, array or function
|
||||
// value: every JS object is truthy): `o && x` evaluates `o` for its
|
||||
// effects and answers `x`; `o || x` answers `o` and never evaluates
|
||||
// `x`. supports-color's `stream && stream.isTTY` is the shape. Ahead
|
||||
// of the union lift below exactly where that lift would store the
|
||||
// object in a result union with no arm for it; a union holding the
|
||||
// operand keeps the lift.
|
||||
if (
|
||||
REF_TRUTHY_KINDS.has(kind) &&
|
||||
(target?.kind !== "union" ||
|
||||
(op === ts.SyntaxKind.AmpersandAmpersandToken &&
|
||||
lowerer.armTag(target.unionId, left.type) < 0))
|
||||
) {
|
||||
const and = op === ts.SyntaxKind.AmpersandAmpersandToken;
|
||||
const answer = and ? right : left;
|
||||
const answerT = target ?? answer.type;
|
||||
if (answerT.kind !== "void") {
|
||||
const value = lowerer.coerceInto(and ? expr.right : expr.left, answer, answerT);
|
||||
if (!and) return value;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: left, loc }],
|
||||
result: value,
|
||||
type: answerT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (target?.kind === "union") {
|
||||
lowerer.requireTruthyUnion(target.unionId, expr);
|
||||
return {
|
||||
@@ -15465,20 +15457,6 @@ export function fieldGetExpr(
|
||||
const helper = lowerer.deferredReadHelper(target.fieldType.unionId, inner, loc);
|
||||
if (helper) return { kind: "call", callee: helper, args: [read], type: inner, loc };
|
||||
}
|
||||
// An early-read field (ClassInfo.earlyReadFields) yields its undefined
|
||||
// where the use reads it as Node does; elsewhere the declared type is
|
||||
// extracted with a check.
|
||||
if (
|
||||
target.fieldType.kind === "union" &&
|
||||
lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true &&
|
||||
!observesEarlyRead(blame)
|
||||
) {
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(blame));
|
||||
if (declared && declared.kind !== "union") {
|
||||
const helper = lowerer.narrowedArmHelper(target.fieldType.unionId, declared, loc);
|
||||
if (helper) return lowerer.checkedNarrowCall(helper, read, declared, loc);
|
||||
}
|
||||
}
|
||||
return read;
|
||||
}
|
||||
return {
|
||||
@@ -16019,22 +15997,6 @@ function lowerFieldCompoundValue(
|
||||
if (op === "+" && read.type.kind === "string") {
|
||||
appended = lowerer.ensureString(rhs, rhsNode ?? access);
|
||||
value = { kind: "strConcat", left: read, right: appended, type: STRING, loc };
|
||||
} else if (
|
||||
op === "+" &&
|
||||
read.type.kind === "union" &&
|
||||
lowerer.stripUndefinedArm(read.type).kind === "string" &&
|
||||
target.container === "class" &&
|
||||
lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true
|
||||
) {
|
||||
// A string field read before its assignment holds undefined, which
|
||||
// concatenates as "undefined".
|
||||
value = {
|
||||
kind: "strConcat",
|
||||
left: lowerer.ensureString(read, access),
|
||||
right: lowerer.ensureString(rhs, rhsNode ?? access),
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
} else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") {
|
||||
value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc };
|
||||
} else if (bigintValue) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* flow (if/while/for/for-of/do, switch, try/catch, abrupt completions),
|
||||
* explicit resource management, and blocked-binding poisoning. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { processDataMemberValue } from "./builtins/process.js";
|
||||
import { requireInitFlagStmt } from "./require-initialization.js";
|
||||
import { isJsonRequireDecl } from "./json-require.js";
|
||||
import { bindingSource } from "../binding-source.js";
|
||||
@@ -3277,6 +3278,9 @@ function stdlibGlobalTokenDestructure(
|
||||
token: string;
|
||||
alias: string | null;
|
||||
g: IrGlobal | undefined;
|
||||
/** A data member's value (`const { env } = process`), bound instead
|
||||
* of a token. */
|
||||
value?: IrExpr;
|
||||
}[] = [];
|
||||
for (const el of decl.name.elements) {
|
||||
if (el.name === undefined) continue;
|
||||
@@ -3312,8 +3316,9 @@ function stdlibGlobalTokenDestructure(
|
||||
"destructuring with computed keys that do not fold to one property name",
|
||||
);
|
||||
}
|
||||
let token: string;
|
||||
let token = "";
|
||||
let alias: string | null = null;
|
||||
let value: IrExpr | null = null;
|
||||
if (globalName === "globalThis") {
|
||||
// The member must BE a stdlib global (user script vars are
|
||||
// properties of `typeof globalThis` too — their values are real
|
||||
@@ -3333,6 +3338,13 @@ function stdlibGlobalTokenDestructure(
|
||||
token = `[builtin console.${propName}]`;
|
||||
} else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) {
|
||||
token = `[builtin ${globalName}.${propName}]`;
|
||||
} else if (
|
||||
globalName === "process" &&
|
||||
ts.isIdentifier(el.name) &&
|
||||
(value = processDataMemberValue(lowerer, propName, el.name, locOf(el))) !== null
|
||||
) {
|
||||
// A data member reads its value once, exactly like the spelled
|
||||
// `process.<member>` read the destructuring performs.
|
||||
} else {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
@@ -3349,6 +3361,10 @@ function stdlibGlobalTokenDestructure(
|
||||
);
|
||||
}
|
||||
const g = lowerer.globalsBySymbol.get(symbol);
|
||||
if (value !== null) {
|
||||
binds.push({ name: el.name, token, alias, g, value });
|
||||
continue;
|
||||
}
|
||||
if (g !== undefined && g.type.kind !== "string" && g.type.kind !== "dyn") {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
@@ -3361,6 +3377,21 @@ function stdlibGlobalTokenDestructure(
|
||||
const out: IrStmt[] = [];
|
||||
for (const b of binds) {
|
||||
const loc = locOf(b.name);
|
||||
if (b.value !== undefined) {
|
||||
if (b.g !== undefined) {
|
||||
out.push({
|
||||
kind: "assign",
|
||||
localId: b.g.id,
|
||||
value: lowerer.coerceInto(b.name, b.value, b.g.type),
|
||||
...(b.g.tdz ? { initializes: true as const } : {}),
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
const local = lowerer.declareLocal(b.name, b.name.text, b.value.type, isLet);
|
||||
out.push({ kind: "varDecl", localId: local.id, init: b.value, loc });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const token: IrExpr = { kind: "strLit", value: b.token, type: STRING, loc };
|
||||
if (b.alias !== null) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(b.name);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
|
||||
import {
|
||||
lifetimeRegionType,
|
||||
lowerLifetimeRegionCall,
|
||||
@@ -2902,6 +2903,8 @@ export class Lowerer {
|
||||
this.fileTag.set(sf, sf === entry ? "" : `%m${i}.`);
|
||||
});
|
||||
if (this.moduleOrder.length === 0) this.fileTag.set(entry, "");
|
||||
const jsDynamic = jsDynamicDeclarations(this.moduleOrder, this.checker);
|
||||
if (jsDynamic.size > 0) this.typeCtx.jsDynamicDeclaration = (decl) => jsDynamic.has(decl);
|
||||
this.registerBuiltinErrorClasses();
|
||||
registerBuiltinEmitterClass(this);
|
||||
registerBuiltinStreamClasses(this);
|
||||
@@ -3976,18 +3979,6 @@ export class Lowerer {
|
||||
if (!s) return null;
|
||||
return s.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(s) : s;
|
||||
};
|
||||
// Fields that can be read before their assignment hold undefined then;
|
||||
// their reads are runtime-optional values like missing array reads.
|
||||
for (const info of this.classes.values()) {
|
||||
if (!info.earlyReadFields || !info.decl?.name) continue;
|
||||
const classSymbol = symbolOf(info.decl.name);
|
||||
if (!classSymbol) continue;
|
||||
const instance = this.checker.getDeclaredTypeOfSymbol(classSymbol);
|
||||
for (const name of info.earlyReadFields) {
|
||||
const property = this.checker.getPropertyOfType(instance, name);
|
||||
if (property) optionalSymbols.add(property);
|
||||
}
|
||||
}
|
||||
const signatureOf = (node: ts.Identifier): FnSig | null => {
|
||||
const symbol = symbolOf(node);
|
||||
if (!symbol) return null;
|
||||
|
||||
@@ -15,7 +15,21 @@ export function isPrunedNpmReexport(program: ts.Program, stmt: ts.ExportDeclarat
|
||||
return program.analysis.prunedNpmReexports.has(stmt);
|
||||
}
|
||||
|
||||
function packageJson(path: string): Record<string, unknown> | null {
|
||||
type PackageJson = Record<string, unknown> | null;
|
||||
|
||||
/** Parsed package.json files by path, kept for one plan: every purity check
|
||||
* reads the metadata of its file's package again. */
|
||||
type PackageJsonCache = Map<string, PackageJson>;
|
||||
|
||||
function packageJson(path: string, cache: PackageJsonCache): PackageJson {
|
||||
const cached = cache.get(path);
|
||||
if (cached !== undefined) return cached;
|
||||
const json = readPackageJson(path);
|
||||
cache.set(path, json);
|
||||
return json;
|
||||
}
|
||||
|
||||
function readPackageJson(path: string): PackageJson {
|
||||
const source = trackedReadFile(path);
|
||||
if (source === null) return null;
|
||||
try {
|
||||
@@ -58,12 +72,16 @@ function sideEffectPattern(pattern: string): RegExp {
|
||||
/** A nested format scope such as dist/esm/package.json is not the package's
|
||||
* published metadata. Stay inside the resolved package while finding its
|
||||
* named root, including pnpm's realpath and workspace-linked installs. */
|
||||
function packageRootJsonPath(fromFile: string, packageName: string): string | null {
|
||||
function packageRootJsonPath(
|
||||
fromFile: string,
|
||||
packageName: string,
|
||||
cache: PackageJsonCache,
|
||||
): string | null {
|
||||
let dir = dirname(resolve(fromFile));
|
||||
let root: string | null = null;
|
||||
while (npmPackageNameOf(dir.replaceAll("\\", "/")) === packageName) {
|
||||
const path = join(dir, "package.json");
|
||||
if (packageJson(path)?.["name"] === packageName) root = path;
|
||||
if (packageJson(path, cache)?.["name"] === packageName) root = path;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
@@ -73,9 +91,13 @@ function packageRootJsonPath(fromFile: string, packageName: string): string | nu
|
||||
|
||||
// An external package's executable sources may not be in this program.
|
||||
// Preserve the conservative whole-dependency promise in that case.
|
||||
function pureExternalPackage(path: string, visiting = new Set<string>()): boolean {
|
||||
function pureExternalPackage(
|
||||
path: string,
|
||||
cache: PackageJsonCache,
|
||||
visiting = new Set<string>(),
|
||||
): boolean {
|
||||
if (visiting.has(path)) return true;
|
||||
const json = packageJson(path);
|
||||
const json = packageJson(path, cache);
|
||||
const value = json?.["sideEffects"];
|
||||
if (json === null || !(value === false || (Array.isArray(value) && value.length === 0)))
|
||||
return false;
|
||||
@@ -85,8 +107,8 @@ function pureExternalPackage(path: string, visiting = new Set<string>()): boolea
|
||||
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
|
||||
for (const name of Object.keys(dependencies)) {
|
||||
const resolved = resolveBareModule(path, name, "js-only");
|
||||
const root = resolved === null ? null : packageRootJsonPath(resolved.typesFile, name);
|
||||
if (root === null || !pureExternalPackage(root, visiting)) return false;
|
||||
const root = resolved === null ? null : packageRootJsonPath(resolved.typesFile, name, cache);
|
||||
if (root === null || !pureExternalPackage(root, cache, visiting)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -135,10 +157,11 @@ export function planNpmStaticReexports(
|
||||
for (const path of extraRoots) request(program.getSourceFile(path) ?? null, null);
|
||||
|
||||
const pureMemo = new Map<ts.SourceFile, boolean>();
|
||||
const packageJsons: PackageJsonCache = new Map();
|
||||
const filePure = (sf: ts.SourceFile): boolean => {
|
||||
const pkg = npmPackageNameOf(sf.fileName) ?? npmStaticPackageOfPath(sf.fileName);
|
||||
const path = pkg === null ? null : packageRootJsonPath(sf.fileName, pkg);
|
||||
const json = path === null ? null : packageJson(path);
|
||||
const path = pkg === null ? null : packageRootJsonPath(sf.fileName, pkg, packageJsons);
|
||||
const json = path === null ? null : packageJson(path, packageJsons);
|
||||
const value = json?.["sideEffects"];
|
||||
if (value === false) return true;
|
||||
if (
|
||||
@@ -195,8 +218,10 @@ export function planNpmStaticReexports(
|
||||
const resolved = resolveBareModule(sf.fileName, edge.spec, "js-only");
|
||||
const pkg = resolved === null ? null : npmPackageNameOf(resolved.typesFile);
|
||||
const root =
|
||||
pkg === null || resolved === null ? null : packageRootJsonPath(resolved.typesFile, pkg);
|
||||
return root !== null && pureExternalPackage(root);
|
||||
pkg === null || resolved === null
|
||||
? null
|
||||
: packageRootJsonPath(resolved.typesFile, pkg, packageJsons);
|
||||
return root !== null && pureExternalPackage(root, packageJsons);
|
||||
});
|
||||
};
|
||||
const pure = visit(root);
|
||||
|
||||
@@ -190,6 +190,10 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
for (const value of Object.values(obj)) transform(value);
|
||||
};
|
||||
if (pkg["exports"] !== undefined) transform(pkg["exports"]);
|
||||
// The "imports" field maps "#" specifiers with the same condition objects
|
||||
// (chalk 5's `"#supports-color": { node, default }`), so Node's "node"
|
||||
// target hoists there too.
|
||||
if (pkg["imports"] !== undefined) transform(pkg["imports"]);
|
||||
}
|
||||
|
||||
/** npmStaticTransformPkgJson over a package.json TEXT (the fs shadow's
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
setProjectPathMappings,
|
||||
setProjectRealm,
|
||||
} from "./resolve.js";
|
||||
import { setNpmStaticPackages } from "./npm-static.js";
|
||||
|
||||
const fixturesRoot = join(import.meta.dirname, "../../../..", "tests/fixtures");
|
||||
|
||||
@@ -60,3 +61,56 @@ test("source-only workspace packages are project modules while JavaScript packag
|
||||
clearResolveCaches();
|
||||
}
|
||||
});
|
||||
|
||||
test("package imports inside a --npm-static package answer the shipped JavaScript", async () => {
|
||||
// chalk 5's shape: a declaration twin beside every runtime file, and a
|
||||
// `{ node, default }` imports entry whose node target is what Node runs.
|
||||
const dir = realpathSync(await mkdtemp(join(tmpdir(), "scriptc-resolve-imports-")));
|
||||
try {
|
||||
const pkg = join(dir, "node_modules", "colors");
|
||||
const vendor = join(pkg, "source", "vendor");
|
||||
await mkdir(join(vendor, "styles"), { recursive: true });
|
||||
await mkdir(join(vendor, "detect"), { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(pkg, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "colors",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
exports: "./source/index.js",
|
||||
imports: {
|
||||
"#styles": "./source/vendor/styles/index.js",
|
||||
"#detect": {
|
||||
node: "./source/vendor/detect/index.js",
|
||||
default: "./source/vendor/detect/browser.js",
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
writeFile(join(pkg, "source", "index.js"), 'import styles from "#styles";\n'),
|
||||
writeFile(join(pkg, "source", "index.d.ts"), "export {};\n"),
|
||||
writeFile(join(vendor, "styles", "index.js"), "export default {};\n"),
|
||||
writeFile(join(vendor, "styles", "index.d.ts"), "export {};\n"),
|
||||
writeFile(join(vendor, "detect", "index.js"), "export default 1;\n"),
|
||||
writeFile(join(vendor, "detect", "index.d.ts"), "export {};\n"),
|
||||
writeFile(join(vendor, "detect", "browser.js"), "export default 0;\n"),
|
||||
writeFile(join(vendor, "detect", "browser.d.ts"), "export {};\n"),
|
||||
]);
|
||||
const from = join(pkg, "source", "index.js");
|
||||
|
||||
clearResolveCaches();
|
||||
setNpmStaticPackages([]);
|
||||
expect(resolveProjectModule(from, "#styles")).toBe(join(vendor, "styles", "index.d.ts"));
|
||||
|
||||
clearResolveCaches();
|
||||
setNpmStaticPackages(["colors"]);
|
||||
expect(resolveProjectModule(from, "#styles")).toBe(join(vendor, "styles", "index.js"));
|
||||
expect(resolveProjectModule(from, "#detect")).toBe(join(vendor, "detect", "index.js"));
|
||||
expect(resolveProjectModule(from, "#missing")).toBeNull();
|
||||
} finally {
|
||||
setNpmStaticPackages([]);
|
||||
clearResolveCaches();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -617,6 +617,18 @@ export function resolveProjectImport(fromFile: string, specifier: string): strin
|
||||
// Node's validity rule: only the bare "#" is never resolvable ("#/..."
|
||||
// matches "#/*"-pattern keys on current Node).
|
||||
if (specifier === "#") return null;
|
||||
// Inside an opted-in --npm-static package, an imports entry resolves
|
||||
// the way the tsgo host's fs shadow does: the transformed document
|
||||
// (the "node" condition hoisted), the runtime JS conditions, and the
|
||||
// shipped JS rather than a declaration twin (see resolveRelativeModule).
|
||||
if (npmStaticPackageOfPath(resolve(fromFile)) !== null) {
|
||||
const transformed = JSON.parse(JSON.stringify(pkg)) as PkgJson & Record<string, unknown>;
|
||||
npmStaticTransformPkgJson(transformed);
|
||||
const jsTarget = resolvePackageImports(transformed.imports, specifier, JS_ONLY_CONDITIONS);
|
||||
if (jsTarget === null || !jsTarget.startsWith("./")) return null;
|
||||
const jsPath = join(pkgDir, jsTarget);
|
||||
return normalizeResolvedAnswer(loadAsJsFile(jsPath) ?? loadAsJsDirectory(jsPath));
|
||||
}
|
||||
target = resolvePackageImports(pkg.imports, specifier, EXPORT_CONDITIONS);
|
||||
// An imports target that is not "./"-relative is PACKAGE_RESOLVE'd as
|
||||
// a bare specifier (Node's PACKAGE_IMPORTS_EXPORTS_RESOLVE): the
|
||||
|
||||
@@ -996,6 +996,10 @@ export type TypeParamTsResolver = (t: ts.Type) => ts.Type | null;
|
||||
* one new field here, not a new parameter at every call site. */
|
||||
export interface TypeMapperCtx {
|
||||
checker: ts.TypeChecker;
|
||||
/** JavaScript declarations whose instance or object-literal types only
|
||||
* the checked-dynamic representation holds (js-dynamic-objects.ts).
|
||||
* Absent when the program has none: every mapping is unchanged. */
|
||||
jsDynamicDeclaration?: (decl: ts.Node) => boolean;
|
||||
/** Set while mapping a generic callable member's erased type
|
||||
* (erasedGenericMemberType): the signature's own type parameters
|
||||
* resolve to `unknown` instead of refusing the mapping. */
|
||||
@@ -1368,6 +1372,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const intrinsic = type.getSymbol();
|
||||
if (intrinsic?.name === "globalThis" && checker.declarationsOf(intrinsic).length === 0)
|
||||
return DYN;
|
||||
if (ctx.jsDynamicDeclaration !== undefined && intrinsic !== undefined) {
|
||||
const objectLiteral = (intrinsic.flags & ts.SymbolFlags.ObjectLiteral) !== 0;
|
||||
const classInstance =
|
||||
(intrinsic.flags & ts.SymbolFlags.Class) !== 0 && type.isClassOrInterface();
|
||||
if (
|
||||
(objectLiteral || classInstance) &&
|
||||
checker.declarationsOf(intrinsic).some((decl) => ctx.jsDynamicDeclaration!(decl))
|
||||
)
|
||||
return DYN;
|
||||
}
|
||||
{
|
||||
const moduleId = ctx.moduleNamespaceId?.(type) ?? null;
|
||||
if (moduleId !== null) return { kind: "moduleNs", moduleId };
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
import type { IrModule } from "../src/ir/ir.js";
|
||||
|
||||
async function lower(source: string, file = "main.ts"): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-early-field-reads-"));
|
||||
try {
|
||||
const entry = join(dir, file),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return module;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Field name → IR kind for a class. */
|
||||
function fields(module: IrModule, name: string): string {
|
||||
const cls = module.classes.find((candidate) => candidate.name === name);
|
||||
expect(cls, name).toBeDefined();
|
||||
return cls!.fields
|
||||
.filter((field) => !field.name.startsWith("%"))
|
||||
.map((field) => `${field.name}:${field.type.kind}`)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
test("fields assigned after construction can call an override admit undefined", async () => {
|
||||
const module = await lower(`
|
||||
abstract class Base {
|
||||
constructor() {
|
||||
this.describe();
|
||||
}
|
||||
abstract describe(): void;
|
||||
}
|
||||
class Panel extends Base {
|
||||
label: string;
|
||||
width: number;
|
||||
count = 0;
|
||||
constructor() {
|
||||
super();
|
||||
this.label = "panel";
|
||||
this.width = 2;
|
||||
}
|
||||
describe(): void {
|
||||
console.log(this.label, this.width, this.count);
|
||||
}
|
||||
}
|
||||
class Plain {
|
||||
label: string;
|
||||
width: number;
|
||||
constructor() {
|
||||
this.label = "plain";
|
||||
this.width = 1;
|
||||
this.describe();
|
||||
}
|
||||
describe(): void {
|
||||
console.log(this.label, this.width);
|
||||
}
|
||||
}
|
||||
new Panel();
|
||||
new Plain();
|
||||
`);
|
||||
expect(fields(module, "Panel")).toBe("label:union width:union count:union");
|
||||
expect(fields(module, "Plain")).toBe("label:string width:f64");
|
||||
});
|
||||
|
||||
test("JavaScript fields created after the constructor calls a method admit undefined", async () => {
|
||||
const module = await lower(
|
||||
`
|
||||
class Widget {
|
||||
constructor() {
|
||||
this.ready = true;
|
||||
this.render();
|
||||
this.title = "widget";
|
||||
}
|
||||
render() {
|
||||
console.log(this.ready, this.title);
|
||||
}
|
||||
}
|
||||
new Widget();
|
||||
`,
|
||||
"main.js",
|
||||
);
|
||||
expect(fields(module, "Widget")).toBe("ready:bool title:union");
|
||||
});
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5943-fields-read-before-assignment.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5944-js-fields-read-before-assignment.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5980-cycle-components.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5996-rejections-between-macrotasks.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-builder-string-method-names/builder.js",
|
||||
"<repo>/tests/corpus/js-builder-string-method-names/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-callable-builder.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-class-constructor-returns/factory.js",
|
||||
"<repo>/tests/corpus/js-class-constructor-returns/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-function-prototype-value.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-number-parse-dynamic.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-process-destructure.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-reshaped-object-literal.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/truthy-object-logical.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/chalk5/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -4464,7 +4464,9 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
|
||||
}
|
||||
scr_firing_id = 0;
|
||||
if (scr_exc_pending()) return false; /* uncaught throw in a callback: main handles it */
|
||||
if (scr_ready_len > 0 || scr_nt_head != NULL) break; /* drain ticks and microtasks before more timers */
|
||||
/* Drain ticks and microtasks, and report the rejections the callback
|
||||
* left, before more timers (Node's runNextTicks between timers). */
|
||||
if (scr_ready_len > 0 || scr_nt_head != NULL || scr_nunhandled > 0) break;
|
||||
}
|
||||
/* The check phase: immediates queued BEFORE this phase started run
|
||||
* now, FIFO; ones a callback queues wait for the next turn (the end
|
||||
@@ -4474,7 +4476,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
|
||||
* every macrotask; pending ticks or a woken fiber restart the turn
|
||||
* first (the timer loop's break above), so the phase only runs on a
|
||||
* drained checkpoint. */
|
||||
if (scr_ready_len == 0 && scr_nt_head == NULL && scr_pending_immediates > 0) {
|
||||
if (scr_ready_len == 0 && scr_nt_head == NULL && scr_nunhandled == 0 && scr_pending_immediates > 0) {
|
||||
size_t end = scr_nimmediates;
|
||||
while (scr_immediates_head < end) {
|
||||
size_t i = scr_immediates_head++;
|
||||
@@ -4490,6 +4492,13 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
|
||||
scr_closure_release(cb);
|
||||
if (scr_exc_pending()) return false; /* uncaught throw: main handles it */
|
||||
if (!scr_loop_run_checkpoint()) return false;
|
||||
/* Node's runNextTicks between immediates also reports the
|
||||
* rejections the callback left before the next one runs; a
|
||||
* listener's jobs drain before the next report. */
|
||||
while (scr_nunhandled > 0 && (top_level == NULL || top_level->state != SCR_PROM_REJECTED)) {
|
||||
if (scr_report_unhandled_rejections()) return true;
|
||||
if (!scr_loop_run_checkpoint()) return false;
|
||||
}
|
||||
if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break;
|
||||
}
|
||||
/* Fully drained: reset the cursor so the array reuses its slots. */
|
||||
|
||||
@@ -26,14 +26,19 @@
|
||||
* count was already given up (which would over-decrement and free live
|
||||
* data).
|
||||
*
|
||||
* The main graph phases keep a bounded depth-first walk and defer deeper
|
||||
* traces to reusable worklists. Ordinary RC destruction and cross-generation
|
||||
* releases are separate paths. White candidates are recorded during scan
|
||||
* and filtered after outside roots settle, avoiding a separate gather walk.
|
||||
* markGray also records the objects it grays and counts those still
|
||||
* externally referenced (rc > 0 after the trial decrements). When none is,
|
||||
* every gray object is garbage, which is what scan would find, so the pass
|
||||
* whitens the recorded set and skips scan.
|
||||
* The graph phases keep a bounded depth-first walk and defer deeper traces
|
||||
* to reusable worklists. Ordinary RC destruction and cross-generation
|
||||
* releases are separate paths. markGray records the objects it grays (up
|
||||
* to SCR_CYC_GRAY_LIST_MAX) and counts those still externally referenced
|
||||
* (rc > 0 after the trial decrements), per pass and per component: the
|
||||
* objects first reached from one candidate. A component with none is
|
||||
* garbage, which is what scan would find, so the pass dooms it at once.
|
||||
* For the others, scan blackens from each recorded object that is still
|
||||
* externally referenced, which restores exactly the survivors, and what
|
||||
* stays gray is the garbage: no walk whitens it. Only a pass too large to
|
||||
* record walks scan from the candidates, whitening as it goes, and then
|
||||
* filters the white set after outside roots settle, avoiding a separate
|
||||
* gather walk.
|
||||
*
|
||||
* ── generations ──────────────────────────────────────────────────────
|
||||
* A pass costs O(objects reachable from its candidates), and a candidate
|
||||
@@ -225,6 +230,9 @@ typedef struct {
|
||||
size_t trigger;
|
||||
size_t alloc_trigger;
|
||||
size_t gray_referenced;
|
||||
size_t comp_referenced;
|
||||
uint32_t gray_comp;
|
||||
ScrVec live_ranges;
|
||||
size_t scheduled_mature_age;
|
||||
size_t xg_freed;
|
||||
size_t mature_dead;
|
||||
@@ -256,6 +264,9 @@ static _Thread_local ScrCycThread scr_cyc_thread = {.gen_limit = SCR_CYC_OLD};
|
||||
#define scr_cyc_trigger (scr_cyc_thread.trigger)
|
||||
#define scr_cyc_alloc_trigger (scr_cyc_thread.alloc_trigger)
|
||||
#define scr_gray_referenced (scr_cyc_thread.gray_referenced)
|
||||
#define scr_comp_referenced (scr_cyc_thread.comp_referenced)
|
||||
#define scr_gray_comp (scr_cyc_thread.gray_comp)
|
||||
#define scr_live_ranges (scr_cyc_thread.live_ranges)
|
||||
#define scr_cyc_scheduled_mature_age (scr_cyc_thread.scheduled_mature_age)
|
||||
#define scr_xg_freed (scr_cyc_thread.xg_freed)
|
||||
#define scr_cyc_mature_dead (scr_cyc_thread.mature_dead)
|
||||
@@ -347,7 +358,7 @@ void *scr_cyc_alloc(size_t size, ScrCycType *type) {
|
||||
|
||||
void scr_cyc_free(void *obj) { scr_cyc_free_inline(obj); }
|
||||
|
||||
static void scr_cyc_grow(ScrVec *vec) {
|
||||
static __attribute__((noinline, cold)) void scr_cyc_grow(ScrVec *vec) {
|
||||
/* Candidate positions are 31-bit (ScrCycHdr.buf). */
|
||||
if (vec->cap > (size_t)(SCR_CYC_BUFFERED_BIT - 1) / 2) scr_cyc_oom();
|
||||
vec->cap = vec->cap ? vec->cap * 2 : 64;
|
||||
@@ -368,6 +379,16 @@ static inline void scr_cyc_push_work(ScrVec *vec, void *obj) {
|
||||
if (vec->n > vec->used) vec->used = vec->n;
|
||||
}
|
||||
|
||||
/* The same push inline, for the gray list markGray fills once per object;
|
||||
* the pass records the list's high-water mark itself (scr_cyc_note_used). */
|
||||
static inline void scr_cyc_push_gray(ScrVec *vec, void *obj) {
|
||||
if (__builtin_expect(vec->n == vec->cap, 0)) scr_cyc_grow(vec);
|
||||
vec->v[vec->n++] = obj;
|
||||
}
|
||||
static inline void scr_cyc_note_used(ScrVec *vec) {
|
||||
if (vec->n > vec->used) vec->used = vec->n;
|
||||
}
|
||||
|
||||
/* ── the candidate-root buffers (one per generation) ──────────────────── */
|
||||
|
||||
#ifndef SCR_WORKERS
|
||||
@@ -392,7 +413,6 @@ static SCR_TL ScrVec scr_promote;
|
||||
static SCR_TL ScrVec scr_pending;
|
||||
static SCR_TL ScrVec scr_restore_pending;
|
||||
#endif
|
||||
#define SCR_CYC_WALK_DEPTH 64
|
||||
|
||||
/* The gathered white set (freed after the walk completes). */
|
||||
#ifndef SCR_WORKERS
|
||||
@@ -547,9 +567,19 @@ static SCR_TL size_t scr_cyc_alloc_trigger = 0;
|
||||
#endif
|
||||
/* Gray objects whose count is above zero after the trial decrements so far
|
||||
* (see scr_mg_visit). markGray records the gray objects in scr_white while
|
||||
* there are fewer than SCR_CYC_GRAY_LIST_MAX; a larger walk runs scan. */
|
||||
* there are fewer than SCR_CYC_GRAY_LIST_MAX; a larger walk runs scan.
|
||||
*
|
||||
* The same count is kept per component (see "components" at scr_cyc_pass):
|
||||
* the objects one candidate's walk grays first carry that walk's number in
|
||||
* `buf` (gray objects are never buffered during a pass), and
|
||||
* scr_comp_referenced counts those still externally referenced.
|
||||
* scr_live_ranges holds (start, end) ranges of scr_white whose component
|
||||
* was externally referenced and is left to scan. */
|
||||
#ifndef SCR_WORKERS
|
||||
static SCR_TL size_t scr_gray_referenced = 0;
|
||||
static SCR_TL size_t scr_comp_referenced = 0;
|
||||
static SCR_TL uint32_t scr_gray_comp = 0;
|
||||
static SCR_TL ScrVec scr_live_ranges;
|
||||
#endif
|
||||
#define SCR_CYC_GRAY_LIST_MAX 65536
|
||||
|
||||
@@ -982,42 +1012,103 @@ void scr_cyc_on_release(void *obj) {
|
||||
((child) == NULL || SCR_RC(child) == SIZE_MAX \
|
||||
|| scr_cyc_gen(scr_cyc_hdr(child)) > scr_gen_limit)
|
||||
|
||||
/* Trace callbacks forward an integer depth through their opaque context;
|
||||
* it is never dereferenced. The ABI already carries it through every edge. */
|
||||
static void scr_mark_gray(void *obj, uintptr_t depth);
|
||||
/* The walks are depth-first and recursive up to SCR_CYC_WALK_DEPTH, where
|
||||
* an object is queued instead (scr_pending, or scr_restore_pending for the
|
||||
* restoration, which can run inside scan) and traced from the queue. Trace
|
||||
* callbacks forward that integer depth through their opaque context; it is
|
||||
* never dereferenced. */
|
||||
#define SCR_CYC_WALK_DEPTH 64
|
||||
|
||||
/* A walk visits a dense cycle-capable array's children itself: the present
|
||||
* dense elements, which are all scr_arr_trace_v visits when the array has
|
||||
* no sparse entries, properties or metadata (no `ext`). Each walk gets its
|
||||
* own copy, out of line, so the per-edge visitors stay small. */
|
||||
#define SCR_CYC_ARRAY_WALK(name, visit) \
|
||||
static __attribute__((noinline)) void name(const ScrArr *arr, void *ctx) { \
|
||||
size_t end = arr->len < arr->cap ? arr->len : arr->cap; \
|
||||
size_t filled = arr->filled < end ? arr->filled : end; \
|
||||
for (size_t i = 0; i < filled; i++) visit((void *)(uintptr_t)arr->data[i], ctx); \
|
||||
for (size_t i = filled; i < end; i++) \
|
||||
if (arr->present[i] == SCR_ARR_VALUE) visit((void *)(uintptr_t)arr->data[i], ctx); \
|
||||
}
|
||||
|
||||
/* Visit every child of `obj` with VISIT, through ARRAY_WALK for a dense
|
||||
* cycle-capable array and through the type's trace otherwise. */
|
||||
#define SCR_CYC_CHILDREN(obj, type, ARRAY_WALK, VISIT, depth) \
|
||||
do { \
|
||||
if ((type)->kind == SCR_CYC_KIND_ARRAY && ((const ScrArr *)(obj))->ext == NULL) \
|
||||
ARRAY_WALK((const ScrArr *)(obj), (void *)(depth)); \
|
||||
else \
|
||||
(type)->trace((obj), (VISIT), (void *)(depth)); \
|
||||
} while (0)
|
||||
|
||||
static void scr_mg_visit(void *child, void *ctx);
|
||||
static void scr_sb_visit(void *child, void *ctx);
|
||||
static void scr_scan_visit(void *child, void *ctx);
|
||||
SCR_CYC_ARRAY_WALK(scr_mg_array, scr_mg_visit)
|
||||
SCR_CYC_ARRAY_WALK(scr_sb_array, scr_sb_visit)
|
||||
SCR_CYC_ARRAY_WALK(scr_scan_array, scr_scan_visit)
|
||||
|
||||
static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintptr_t depth);
|
||||
static void scr_mg_visit(void *child, void *ctx) {
|
||||
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
|
||||
if (scr_cyc_gen(scr_cyc_hdr(child)) > scr_gen_limit) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(child);
|
||||
if (scr_cyc_gen(h) > scr_gen_limit) {
|
||||
scr_cyc_push_work(&scr_xgen_edges, scr_gray_holder);
|
||||
scr_cyc_push_work(&scr_xgen_edges, child);
|
||||
return;
|
||||
}
|
||||
size_t rc = --SCR_RC(child);
|
||||
if (scr_cyc_color(scr_cyc_hdr(child)) == SCR_CYC_GRAY) {
|
||||
if (rc == 0) scr_gray_referenced--;
|
||||
if (scr_cyc_color(h) == SCR_CYC_GRAY) {
|
||||
if (rc == 0) {
|
||||
scr_gray_referenced--;
|
||||
if (h->buf == scr_gray_comp) scr_comp_referenced--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
scr_mark_gray(child, (uintptr_t)ctx);
|
||||
}
|
||||
static void scr_mark_gray(void *obj, uintptr_t depth) {
|
||||
/* markGray reaches `obj` (its count already trial-decremented): gray it,
|
||||
* number it with the walk's component, record it, count it while it is
|
||||
* externally referenced, and walk its children. */
|
||||
static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintptr_t depth) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (scr_cyc_color(h) == SCR_CYC_GRAY) return;
|
||||
scr_cyc_set_color(h, SCR_CYC_GRAY);
|
||||
/* A full pass takes every buffer, so each buffered object it reaches
|
||||
* leaves its buffer here, in the header write the walk makes anyway. */
|
||||
if (scr_gen_limit == SCR_CYC_OLD && !scr_cyc_take) scr_cyc_clear_buffered(h);
|
||||
if (scr_white.n < SCR_CYC_GRAY_LIST_MAX) scr_cyc_push_work(&scr_white, obj);
|
||||
if (SCR_RC(obj) > 0) scr_gray_referenced++;
|
||||
* leaves its buffer here, in the header write that numbers it anyway. A
|
||||
* limited pass can gray candidates it did not take; they keep their
|
||||
* buffer position and are simply never counted as members. */
|
||||
if (!scr_cyc_is_buffered(h) || (scr_gen_limit == SCR_CYC_OLD && !scr_cyc_take))
|
||||
h->buf = scr_gray_comp;
|
||||
if (scr_white.n < SCR_CYC_GRAY_LIST_MAX) scr_cyc_push_gray(&scr_white, obj);
|
||||
if (SCR_RC(obj) > 0) {
|
||||
scr_gray_referenced++;
|
||||
scr_comp_referenced++;
|
||||
}
|
||||
if (depth == SCR_CYC_WALK_DEPTH) {
|
||||
scr_cyc_push_work(&scr_pending, obj);
|
||||
return;
|
||||
}
|
||||
void *holder = scr_gray_holder;
|
||||
scr_gray_holder = obj;
|
||||
scr_cyc_type(h)->trace(obj, scr_mg_visit, (void *)(depth + 1));
|
||||
const ScrCycType *type = scr_cyc_type(h);
|
||||
SCR_CYC_CHILDREN(obj, type, scr_mg_array, scr_mg_visit, depth + 1);
|
||||
scr_gray_holder = holder;
|
||||
}
|
||||
/* markGray from one (black) candidate, through the queue. */
|
||||
static void scr_mark_gray_from(void *root) {
|
||||
scr_mark_gray(root, 0);
|
||||
while (scr_pending.n) {
|
||||
void *obj = scr_pending.v[--scr_pending.n];
|
||||
const ScrCycType *type = scr_cyc_type(scr_cyc_hdr(obj));
|
||||
scr_gray_holder = obj;
|
||||
SCR_CYC_CHILDREN(obj, type, scr_mg_array, scr_mg_visit, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* scan's restoration: blacken an externally referenced object and
|
||||
* everything it reaches that is not black yet, giving back the trial
|
||||
* decrements of the edges it follows. */
|
||||
static void scr_scan_black(void *obj, uintptr_t depth);
|
||||
static void scr_sb_visit(void *child, void *ctx) {
|
||||
if (SCR_CYC_SKIP(child)) return;
|
||||
@@ -1028,14 +1119,29 @@ static void scr_sb_visit(void *child, void *ctx) {
|
||||
static void scr_scan_black(void *obj, uintptr_t depth) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
scr_cyc_set_color(h, SCR_CYC_BLACK);
|
||||
/* Drop markGray's component number: a survivor leaves the pass unbuffered
|
||||
* unless it is a candidate that a limited pass did not take. */
|
||||
if (!scr_cyc_is_buffered(h)) scr_cyc_clear_buffered(h);
|
||||
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_push_work(&scr_promote, obj);
|
||||
if (depth == SCR_CYC_WALK_DEPTH) {
|
||||
scr_cyc_push_work(&scr_restore_pending, obj);
|
||||
return;
|
||||
}
|
||||
scr_cyc_type(h)->trace(obj, scr_sb_visit, (void *)(depth + 1));
|
||||
const ScrCycType *type = scr_cyc_type(h);
|
||||
SCR_CYC_CHILDREN(obj, type, scr_sb_array, scr_sb_visit, depth + 1);
|
||||
}
|
||||
static void scr_restore_from(void *root) {
|
||||
scr_scan_black(root, 0);
|
||||
while (scr_restore_pending.n) {
|
||||
void *obj = scr_restore_pending.v[--scr_restore_pending.n];
|
||||
const ScrCycType *type = scr_cyc_type(scr_cyc_hdr(obj));
|
||||
SCR_CYC_CHILDREN(obj, type, scr_sb_array, scr_sb_visit, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* scan, walking: restore a gray object that is externally referenced, else
|
||||
* whiten it, record it once (a later outside root can still restore it;
|
||||
* settle filters the list) and walk its children. */
|
||||
static void scr_scan(void *obj, uintptr_t depth);
|
||||
static void scr_scan_visit(void *child, void *ctx) {
|
||||
if (SCR_CYC_SKIP(child)) return;
|
||||
@@ -1045,22 +1151,17 @@ static void scr_scan(void *obj, uintptr_t depth) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (scr_cyc_color(h) != SCR_CYC_GRAY) return;
|
||||
if (SCR_RC(obj) > 0) {
|
||||
scr_scan_black(obj, 0);
|
||||
while (scr_restore_pending.n) {
|
||||
void *next = scr_restore_pending.v[--scr_restore_pending.n];
|
||||
scr_cyc_type(scr_cyc_hdr(next))->trace(next, scr_sb_visit, NULL);
|
||||
}
|
||||
scr_restore_from(obj);
|
||||
return;
|
||||
}
|
||||
scr_cyc_set_color(h, SCR_CYC_WHITE);
|
||||
/* Record each whitened object once. A later outside root can restore it;
|
||||
* filter the list only after every scan finishes. No gather trace needed. */
|
||||
scr_cyc_push_work(&scr_white, obj);
|
||||
if (depth == SCR_CYC_WALK_DEPTH) {
|
||||
scr_cyc_push_work(&scr_pending, obj);
|
||||
return;
|
||||
}
|
||||
scr_cyc_type(h)->trace(obj, scr_scan_visit, (void *)(depth + 1));
|
||||
const ScrCycType *type = scr_cyc_type(h);
|
||||
SCR_CYC_CHILDREN(obj, type, scr_scan_array, scr_scan_visit, depth + 1);
|
||||
}
|
||||
|
||||
/* ── cross-generation edges ───────────────────────────────────────────── */
|
||||
@@ -1138,22 +1239,28 @@ static SCR_CYC_COMPACT void scr_xg_release(void *obj) {
|
||||
* spend their time in the trace and teardown calls they make, so this half is
|
||||
* built for size; the graph walk above it stays fully optimized. Returns how
|
||||
* many objects the pass freed. */
|
||||
static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
|
||||
static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) {
|
||||
size_t dead = 0;
|
||||
scr_cyc_mature_dead = 0;
|
||||
for (size_t i = 0; i < scr_white.n; i++) {
|
||||
scr_cyc_note_used(&scr_white);
|
||||
/* The list holds the dead set: doomed components, and what scan whitened
|
||||
* (black once a later outside root restored it), or, when the pass found
|
||||
* nothing externally referenced, every gray object. `doomed` says that
|
||||
* every entry is a doomed component's already. */
|
||||
for (size_t i = 0; i < scr_white.n && !doomed; i++) {
|
||||
void *obj = scr_white.v[i];
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (scr_cyc_color(h) != SCR_CYC_WHITE) continue;
|
||||
/* A limited pass can reach candidates it did not take: out of their
|
||||
* buffers before they are freed. */
|
||||
if (scr_cyc_take && scr_cyc_is_buffered(h)) scr_cyc_on_dead(obj);
|
||||
scr_cyc_set_color(h, SCR_CYC_DOOMED);
|
||||
if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(scr_cyc_type(h), gen_limit);
|
||||
else scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE;
|
||||
unsigned color = scr_cyc_color(h);
|
||||
if (color == SCR_CYC_BLACK) continue;
|
||||
if (color != SCR_CYC_DOOMED) {
|
||||
/* A limited pass can reach candidates it did not take: out of their
|
||||
* buffers before they are freed. */
|
||||
if (scr_cyc_take && scr_cyc_is_buffered(h)) scr_cyc_on_dead(obj);
|
||||
scr_cyc_set_color(h, SCR_CYC_DOOMED);
|
||||
}
|
||||
scr_white.v[dead++] = obj;
|
||||
}
|
||||
scr_white.n = dead;
|
||||
if (!doomed) scr_white.n = dead;
|
||||
|
||||
/* Pin the cross-generation targets of everything about to be freed, while
|
||||
* `gen` still holds the values the walk filtered on (promotion below is
|
||||
@@ -1217,13 +1324,19 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
|
||||
* over every page the dead structure used, and the structures built from
|
||||
* them, and every later walk over those, would miss the cache on most
|
||||
* objects. In address order the blocks come back in the order the bump
|
||||
* pointers hand out fresh memory. Teardown order is not observable. */
|
||||
* pointers hand out fresh memory. Teardown order is not observable. The
|
||||
* level statistics are counted here, where the teardown reads the type
|
||||
* anyway; they only steer later passes. */
|
||||
size_t freed = scr_white.n;
|
||||
if (scr_white.n >= SCR_CYC_ORDER_MIN)
|
||||
scr_mem_order_for_free(scr_white.v, scr_white.n, sizeof(ScrCycHdr));
|
||||
for (size_t i = 0; i < scr_white.n; i++) {
|
||||
void *obj = scr_white.v[i];
|
||||
scr_cyc_type(scr_cyc_hdr(obj))->free_fn(obj);
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
const ScrCycType *type = scr_cyc_type(h);
|
||||
if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(type, gen_limit);
|
||||
else scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE;
|
||||
type->free_fn(obj);
|
||||
}
|
||||
scr_white.n = 0;
|
||||
|
||||
@@ -1299,6 +1412,7 @@ static size_t scr_cyc_pass_done(size_t freed) {
|
||||
scr_cyc_trim(&scr_restore_pending);
|
||||
scr_cyc_trim(&scr_xgen);
|
||||
scr_cyc_trim(&scr_xgen_edges);
|
||||
scr_cyc_trim(&scr_live_ranges);
|
||||
return freed;
|
||||
}
|
||||
|
||||
@@ -1369,28 +1483,114 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
scr_xgen_edges.n = 0;
|
||||
scr_white.n = 0;
|
||||
scr_gray_referenced = 0;
|
||||
for (size_t i = 0; i < scr_cands.n; i++) scr_mark_gray(scr_cands.v[i], 0);
|
||||
while (scr_pending.n) {
|
||||
void *obj = scr_pending.v[--scr_pending.n];
|
||||
scr_gray_holder = obj;
|
||||
scr_cyc_type(scr_cyc_hdr(obj))->trace(obj, scr_mg_visit, NULL);
|
||||
scr_live_ranges.n = 0;
|
||||
/* Components. Each candidate that no earlier walk reached starts a walk
|
||||
* of its own, and the objects that walk grays first form its component.
|
||||
* A component whose objects all end the walk at count zero is garbage:
|
||||
* a count of zero means every reference to the object was trial-deleted,
|
||||
* i.e. came from an object grayed so far. An earlier component cannot
|
||||
* hold one (the walk that grayed it would have grayed the target too),
|
||||
* so every reference comes from inside the component, and a later walk
|
||||
* cannot add one: its objects would be referencing this component, and
|
||||
* those references are still counted. scan would whiten exactly this
|
||||
* set, so it is doomed at once and scan runs only over the components
|
||||
* that are externally referenced, if any. A limited pass keeps the single
|
||||
* whole-pass decision below (its untaken candidates cannot carry the
|
||||
* component number). */
|
||||
bool split = scr_cyc_take == NULL;
|
||||
uint32_t comp = 0;
|
||||
for (size_t i = 0; i < scr_cands.n; i++) {
|
||||
void *obj = scr_cands.v[i];
|
||||
if (scr_cyc_color(scr_cyc_hdr(obj)) != SCR_CYC_BLACK) continue; /* walked already */
|
||||
size_t start = scr_white.n;
|
||||
scr_gray_comp = split ? ++comp : 0;
|
||||
scr_comp_referenced = 0;
|
||||
scr_mark_gray_from(obj);
|
||||
if (!split) continue;
|
||||
size_t end = scr_white.n;
|
||||
if (scr_comp_referenced == 0 && end < SCR_CYC_GRAY_LIST_MAX) {
|
||||
for (size_t j = start; j < end; j++) scr_cyc_set_color(scr_cyc_hdr(scr_white.v[j]), SCR_CYC_DOOMED);
|
||||
} else if (scr_live_ranges.n && (uintptr_t)scr_live_ranges.v[scr_live_ranges.n - 1] == start) {
|
||||
scr_live_ranges.v[scr_live_ranges.n - 1] = (void *)(uintptr_t)end;
|
||||
} else {
|
||||
scr_cyc_push_work(&scr_live_ranges, (void *)(uintptr_t)start);
|
||||
scr_cyc_push_work(&scr_live_ranges, (void *)(uintptr_t)end);
|
||||
}
|
||||
}
|
||||
if (scr_gray_referenced == 0 && scr_white.n < SCR_CYC_GRAY_LIST_MAX) {
|
||||
for (size_t i = 0; i < scr_white.n; i++) scr_cyc_set_color(scr_cyc_hdr(scr_white.v[i]), SCR_CYC_WHITE);
|
||||
return scr_cyc_pass_done(scr_cyc_settle(gen_limit));
|
||||
scr_cyc_note_used(&scr_white);
|
||||
/* Nothing externally referenced at all: every gray object is garbage,
|
||||
* including components that looked referenced only because a later
|
||||
* component held them. settle dooms the recorded set. */
|
||||
if (scr_gray_referenced == 0 && scr_white.n < SCR_CYC_GRAY_LIST_MAX)
|
||||
return scr_cyc_pass_done(scr_cyc_settle(gen_limit, split && scr_live_ranges.n == 0));
|
||||
|
||||
/* scan, from the recorded set when markGray recorded every gray object:
|
||||
* the survivors are exactly what the externally referenced gray objects
|
||||
* reach, so blackening from each of them in turn leaves the garbage gray
|
||||
* without walking it again, and settle dooms what is still gray. Only
|
||||
* the referenced components' entries need looking at; the doomed ones
|
||||
* are settled already. */
|
||||
if (scr_white.n < SCR_CYC_GRAY_LIST_MAX) {
|
||||
/* One sweep over the list, keeping the doomed components' entries and,
|
||||
* of the referenced components', only those still gray: a survivor
|
||||
* leaves the list as soon as it is seen black, and an object kept here
|
||||
* that a later restoration blackens is skipped by settle. */
|
||||
size_t kept = 0, from = 0;
|
||||
size_t nranges = split ? scr_live_ranges.n : 2;
|
||||
for (size_t k = 0; k < nranges; k += 2) {
|
||||
size_t start = split ? (uintptr_t)scr_live_ranges.v[k] : 0;
|
||||
size_t end = split ? (uintptr_t)scr_live_ranges.v[k + 1] : scr_white.n;
|
||||
if (start > from) memmove(scr_white.v + kept, scr_white.v + from, (start - from) * sizeof *scr_white.v);
|
||||
kept += start - from;
|
||||
for (size_t j = start; j < end; j++) {
|
||||
void *obj = scr_white.v[j];
|
||||
if (scr_cyc_color(scr_cyc_hdr(obj)) != SCR_CYC_GRAY) continue;
|
||||
if (SCR_RC(obj) > 0) scr_restore_from(obj);
|
||||
else scr_white.v[kept++] = obj;
|
||||
}
|
||||
from = end;
|
||||
}
|
||||
if (scr_white.n > from) memmove(scr_white.v + kept, scr_white.v + from, (scr_white.n - from) * sizeof *scr_white.v);
|
||||
scr_white.n = kept + (scr_white.n - from);
|
||||
return scr_cyc_pass_done(scr_cyc_settle(gen_limit, false));
|
||||
}
|
||||
|
||||
scr_white.n = 0;
|
||||
for (size_t i = 0; i < scr_cands.n; i++) scr_scan(scr_cands.v[i], 0);
|
||||
/* Otherwise scan walks from the candidates. Keep only the doomed
|
||||
* components' entries; scan appends what it whitens. */
|
||||
if (!split) {
|
||||
scr_white.n = 0;
|
||||
} else if (scr_live_ranges.n) {
|
||||
size_t kept = 0, from = 0;
|
||||
for (size_t k = 0; k < scr_live_ranges.n; k += 2) {
|
||||
size_t start = (uintptr_t)scr_live_ranges.v[k], end = (uintptr_t)scr_live_ranges.v[k + 1];
|
||||
memmove(scr_white.v + kept, scr_white.v + from, (start - from) * sizeof *scr_white.v);
|
||||
kept += start - from;
|
||||
from = end;
|
||||
}
|
||||
memmove(scr_white.v + kept, scr_white.v + from, (scr_white.n - from) * sizeof *scr_white.v);
|
||||
scr_white.n = kept + (scr_white.n - from);
|
||||
}
|
||||
/* Externally referenced candidates first: what they restore is never
|
||||
* whitened only to be restored again. */
|
||||
for (size_t i = 0; i < scr_cands.n; i++) {
|
||||
void *obj = scr_cands.v[i];
|
||||
if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_GRAY && SCR_RC(obj) > 0) scr_restore_from(obj);
|
||||
}
|
||||
for (size_t i = 0; i < scr_cands.n; i++) {
|
||||
void *obj = scr_cands.v[i];
|
||||
if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_GRAY) scr_scan(obj, 0);
|
||||
}
|
||||
while (scr_pending.n) {
|
||||
void *obj = scr_pending.v[--scr_pending.n];
|
||||
/* A later outside root may have restored this deferred white node and
|
||||
* its descendants already. Never scan a black node's edges again. */
|
||||
if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_WHITE)
|
||||
scr_cyc_type(scr_cyc_hdr(obj))->trace(obj, scr_scan_visit, NULL);
|
||||
/* A later outside root may have restored this deferred white object and
|
||||
* its descendants already. Never scan a black object's edges again. */
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (scr_cyc_color(h) != SCR_CYC_WHITE) continue;
|
||||
const ScrCycType *type = scr_cyc_type(h);
|
||||
SCR_CYC_CHILDREN(obj, type, scr_scan_array, scr_scan_visit, 1);
|
||||
}
|
||||
|
||||
return scr_cyc_pass_done(scr_cyc_settle(gen_limit));
|
||||
return scr_cyc_pass_done(scr_cyc_settle(gen_limit, false));
|
||||
}
|
||||
|
||||
#ifdef SCR_CYC_TESTING
|
||||
@@ -1425,7 +1625,7 @@ void scr_collect_cycles(void) {
|
||||
void scr_cyc_context_cleanup(void) {
|
||||
scr_collect_cycles();
|
||||
ScrVec *vectors[] = {&scr_roots[SCR_CYC_NURSERY], &scr_roots[SCR_CYC_MATURE],
|
||||
&scr_roots[SCR_CYC_OLD], &scr_cands, &scr_promote, &scr_pending, &scr_restore_pending, &scr_white, &scr_xgen, &scr_xgen_edges,
|
||||
&scr_roots[SCR_CYC_OLD], &scr_cands, &scr_promote, &scr_pending, &scr_restore_pending, &scr_white, &scr_xgen, &scr_xgen_edges, &scr_live_ranges,
|
||||
&scr_pub_holes[SCR_CYC_NURSERY], &scr_pub_holes[SCR_CYC_MATURE], &scr_pub_holes[SCR_CYC_OLD]};
|
||||
for (size_t i = 0; i < sizeof vectors / sizeof vectors[0]; i++) {
|
||||
free(vectors[i]->v);
|
||||
|
||||
@@ -92,6 +92,9 @@ typedef struct ScrCycType {
|
||||
* SCR_CYC_KIND_EMITTED (llvm/alloc.ts FIRST_EMITTED_CYC_KIND). */
|
||||
enum {
|
||||
SCR_CYC_KIND_NONE = 0,
|
||||
/* Also how the collector recognizes a cycle-capable ScrArr, whose dense
|
||||
* elements it visits inline (scr_cycle.c SCR_CYC_CHILDREN): reserved for
|
||||
* scr_array.c's descriptor. */
|
||||
SCR_CYC_KIND_ARRAY,
|
||||
SCR_CYC_KIND_PROMISE,
|
||||
SCR_CYC_KIND_BOX,
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
// @transform-types
|
||||
// Fields that a base constructor (or the constructor itself) can read
|
||||
// through an override or a method call before they are assigned hold
|
||||
// undefined then, as in Node, for every field kind. After construction
|
||||
// the fields read and update normally.
|
||||
class Point {
|
||||
x: number;
|
||||
constructor(x: number) {
|
||||
this.x = x;
|
||||
}
|
||||
}
|
||||
function show(label: string, value: number): string {
|
||||
return `${label}=${value}`;
|
||||
}
|
||||
abstract class Widget {
|
||||
log: string[] = [];
|
||||
constructor() {
|
||||
this.describe("base");
|
||||
}
|
||||
abstract describe(when: string): void;
|
||||
}
|
||||
class Panel extends Widget {
|
||||
label: string;
|
||||
width: number;
|
||||
visible: boolean;
|
||||
origin: Point;
|
||||
items: number[];
|
||||
options: { depth: number };
|
||||
key: string | number;
|
||||
index: Map<string, number>;
|
||||
tags: Set<string>;
|
||||
count = 5;
|
||||
constructor(label: string, readonly id: string) {
|
||||
super();
|
||||
this.label = label;
|
||||
this.width = 10;
|
||||
this.visible = true;
|
||||
this.origin = new Point(1);
|
||||
this.items = [1, 2];
|
||||
this.options = { depth: 2 };
|
||||
this.key = 7;
|
||||
this.index = new Map([["a", 1]]);
|
||||
this.tags = new Set(["t"]);
|
||||
this.describe("ctor");
|
||||
}
|
||||
describe(when: string): void {
|
||||
console.log(when, this.label, this.width, this.visible, this.origin, this.items, this.options, this.key, this.count, this.id);
|
||||
console.log(when, `${this.label}:${this.width}`, this.width + 1, this.count * 2, this.label === undefined, typeof this.items, this.origin?.x, this.options?.depth ?? -1, this.index?.size, this.tags?.has("t"));
|
||||
console.log(when, show(this.label, this.width), String(this.key), [this.label].length);
|
||||
try {
|
||||
console.log(this.label.length, this.items.length);
|
||||
} catch (error) {
|
||||
console.log(when, (error as Error).message);
|
||||
}
|
||||
}
|
||||
grow(): number {
|
||||
this.count++;
|
||||
this.width += 5;
|
||||
this.label += "!";
|
||||
this.items.push(this.width);
|
||||
return this.count + this.width + this.items.length;
|
||||
}
|
||||
}
|
||||
const panel = new Panel("main", "p1");
|
||||
console.log(panel.grow(), panel.label, panel.items, panel.count, panel.width);
|
||||
|
||||
class Plain {
|
||||
total: number;
|
||||
name: string;
|
||||
constructor() {
|
||||
this.total = 1;
|
||||
this.name = "plain";
|
||||
this.bump();
|
||||
}
|
||||
bump(): void {
|
||||
this.total++;
|
||||
}
|
||||
}
|
||||
const plain = new Plain();
|
||||
console.log(plain.total, plain.name);
|
||||
|
||||
class Toucher {
|
||||
constructor() {
|
||||
this.touch();
|
||||
}
|
||||
touch(): void {}
|
||||
}
|
||||
class Counter extends Toucher {
|
||||
count = 0;
|
||||
label: string;
|
||||
items: string[];
|
||||
constructor() {
|
||||
super();
|
||||
this.label = "c";
|
||||
this.items = [];
|
||||
}
|
||||
override touch(): void {
|
||||
this.count++;
|
||||
this.label += "x";
|
||||
console.log(this.count, this.label, Number.isNaN(this.count));
|
||||
try {
|
||||
this.items.push("a");
|
||||
} catch (error) {
|
||||
console.log(error instanceof TypeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
const c = new Counter();
|
||||
c.touch();
|
||||
console.log(c.count, c.label, c.items);
|
||||
|
||||
class Shower {
|
||||
constructor() {
|
||||
this.show();
|
||||
}
|
||||
show(): void {}
|
||||
}
|
||||
class Box<T> extends Shower {
|
||||
label: string;
|
||||
value: T;
|
||||
constructor(value: T) {
|
||||
super();
|
||||
this.label = "box";
|
||||
this.value = value;
|
||||
}
|
||||
override show(): void {
|
||||
console.log("show", this.label, this.value);
|
||||
}
|
||||
}
|
||||
new Box<number>(3).show();
|
||||
new Box<string>("s").show();
|
||||
|
||||
type Ctor<T = {}> = new (...args: any[]) => T;
|
||||
class Viewer {
|
||||
constructor() {
|
||||
this.show();
|
||||
}
|
||||
show(): void {}
|
||||
}
|
||||
function Labeled<B extends Ctor<Viewer>>(Parent: B) {
|
||||
return class extends Parent {
|
||||
label = "mixed";
|
||||
override show(): void {
|
||||
console.log("show", this.label);
|
||||
}
|
||||
};
|
||||
}
|
||||
const L = Labeled(Viewer);
|
||||
new L().show();
|
||||
|
||||
interface Crash { loc: number; message: string }
|
||||
class Setupper {
|
||||
constructor() {
|
||||
this.setup();
|
||||
}
|
||||
setup(): void {}
|
||||
}
|
||||
class Host extends Setupper {
|
||||
crash: Crash | null;
|
||||
name: string | null;
|
||||
constructor(c: Crash | null) {
|
||||
super();
|
||||
this.crash = c;
|
||||
this.name = "host";
|
||||
}
|
||||
override setup(): void {
|
||||
console.log("setup", this.crash, this.name);
|
||||
if (this.crash !== null) {
|
||||
try {
|
||||
console.log(this.crash.loc);
|
||||
} catch (error) {
|
||||
console.log(error instanceof TypeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
report(): string {
|
||||
if (this.crash) return `${this.crash.message} at ${this.crash.loc}`;
|
||||
return this.name === null ? "none" : this.name.toUpperCase();
|
||||
}
|
||||
}
|
||||
console.log(new Host({ loc: 3, message: "boom" }).report(), new Host(null).report());
|
||||
@@ -1,28 +0,0 @@
|
||||
// JavaScript class fields that the constructor creates after it calls a
|
||||
// method, or after a base constructor that does, hold undefined until
|
||||
// they are assigned, as in Node.
|
||||
class Widget {
|
||||
constructor(name) {
|
||||
this.render();
|
||||
this.name = name;
|
||||
this.size = 3;
|
||||
this.title = "widget";
|
||||
this.parts = ["a"];
|
||||
this.meta = { id: 1 };
|
||||
}
|
||||
render() {
|
||||
console.log("render", this.name, this.title, this.size, this.parts, this.meta, this.size + 1, `${this.name}!`);
|
||||
}
|
||||
}
|
||||
class Big extends Widget {
|
||||
constructor() {
|
||||
super("big");
|
||||
this.extra = 2;
|
||||
}
|
||||
render() {
|
||||
super.render();
|
||||
console.log("big", this.extra);
|
||||
}
|
||||
}
|
||||
new Widget("w").render();
|
||||
new Big().render();
|
||||
@@ -0,0 +1,101 @@
|
||||
// Collector passes whose candidates mix dead structures with live ones: the
|
||||
// ring under construction when a pass runs, owners that keep an inner node
|
||||
// of an otherwise dead ring, trees whose child arrays have holes and sparse
|
||||
// slots, and a live structure too large for a pass to record. Each dead
|
||||
// structure is reclaimed whole, and everything still reachable survives
|
||||
// with its contents intact. The sanitized lane's RC audit fails if anything
|
||||
// below leaks or is freed while reachable.
|
||||
|
||||
class Node {
|
||||
next: Node | null = null;
|
||||
kids: (Node | undefined)[] = [];
|
||||
owner: Node | null = null;
|
||||
value: number;
|
||||
constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function ring(size: number, base: number): Node {
|
||||
const first = new Node(base);
|
||||
let last = first;
|
||||
for (let i = 1; i < size; i++) {
|
||||
const node = new Node(base + i);
|
||||
last.next = node;
|
||||
last = node;
|
||||
}
|
||||
last.next = first;
|
||||
return first;
|
||||
}
|
||||
|
||||
function tree(depth: number, owner: Node | null, value: number): Node {
|
||||
const node = new Node(value);
|
||||
node.owner = owner;
|
||||
if (depth > 0) for (let i = 0; i < 3; i++) node.kids.push(tree(depth - 1, node, value * 3 + i));
|
||||
return node;
|
||||
}
|
||||
|
||||
function total(start: Node): number {
|
||||
const seen = new Set<Node>();
|
||||
const stack: Node[] = [start];
|
||||
let sum = 0;
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
if (seen.has(node)) continue;
|
||||
seen.add(node);
|
||||
sum = (sum + node.value) % 1000003;
|
||||
if (node.next) stack.push(node.next);
|
||||
if (node.owner) stack.push(node.owner);
|
||||
for (const kid of node.kids) if (kid) stack.push(kid);
|
||||
}
|
||||
return sum * 1000 + (seen.size % 1000);
|
||||
}
|
||||
|
||||
// Dead rings dropped while the next one is being built; a few are kept.
|
||||
let rings = 0;
|
||||
const kept: Node[] = [];
|
||||
for (let round = 0; round < 3000; round++) {
|
||||
const r = ring(5 + (round % 7), round);
|
||||
rings = (rings + r.next!.value) % 1000003;
|
||||
if (round % 500 === 0) kept.push(r);
|
||||
}
|
||||
console.log("rings", rings, kept.map(total).join(" "));
|
||||
|
||||
// Rings whose head is dropped while an owner keeps a node in the middle:
|
||||
// the ring stays reachable through it and must survive whole.
|
||||
const anchors: Node[] = [];
|
||||
for (let round = 0; round < 400; round++) {
|
||||
let mid = ring(30, round * 100);
|
||||
for (let i = 0; i < 15; i++) mid = mid.next!;
|
||||
if (round % 4 === 0) anchors.push(mid);
|
||||
}
|
||||
let anchored = 0;
|
||||
for (const anchor of anchors) anchored = (anchored + total(anchor)) % 1000003;
|
||||
console.log("anchored", anchors.length, anchored);
|
||||
|
||||
// Trees with holes and sparse slots in their child arrays.
|
||||
let trees = 0;
|
||||
const shaped: Node[] = [];
|
||||
for (let round = 0; round < 300; round++) {
|
||||
const t = tree(4, null, round);
|
||||
if (round % 3 === 0) t.kids.length = 6;
|
||||
if (round % 5 === 0) t.kids[200] = tree(2, t, round + 7);
|
||||
if (round % 7 === 0) t.kids[0]!.kids.length = 1;
|
||||
trees = (trees + total(t)) % 1000003;
|
||||
if (round % 60 === 0) shaped.push(t.kids[2]!);
|
||||
}
|
||||
console.log("trees", trees, shaped.map(total).join(" "));
|
||||
|
||||
// A live tree larger than one pass records, built and read while dead
|
||||
// structures keep dying around it, then dropped.
|
||||
const big = tree(10, null, 1);
|
||||
let churn = 0;
|
||||
for (let round = 0; round < 200; round++) {
|
||||
const r = ring(50, round);
|
||||
const branch = big.kids[round % 3]!.kids[(round >> 2) % 3]!;
|
||||
branch.next = r;
|
||||
churn = (churn + total(r)) % 1000003;
|
||||
if (round % 50 === 0) churn = (churn + total(big)) % 1000003;
|
||||
branch.next = null;
|
||||
}
|
||||
console.log("big", churn, total(big));
|
||||
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
// Unhandled rejections are reported after the callback that left them,
|
||||
// before the next due timer or the next immediate runs (Node's
|
||||
// runNextTicks between macrotasks), also when several timers expire in
|
||||
// the same turn.
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.log('unhandledRejection', reason instanceof Error ? reason.message : JSON.stringify(reason));
|
||||
});
|
||||
process.on('uncaughtException', (err, origin) => console.log(origin, err));
|
||||
|
||||
const start = Date.now();
|
||||
setTimeout(() => void Promise.reject({ a: 1 }), 1);
|
||||
setTimeout(() => void Promise.reject(new TypeError('typed reason')), 2);
|
||||
setTimeout(() => {
|
||||
throw 'thrown string';
|
||||
}, 3);
|
||||
setTimeout(() => {
|
||||
console.log('timer 4');
|
||||
Promise.reject('handled later').catch((e) => console.log('caught', e));
|
||||
}, 4);
|
||||
setTimeout(() => console.log('timer 5'), 5);
|
||||
// Every timer above is due once this returns.
|
||||
while (Date.now() - start < 20) {}
|
||||
|
||||
setTimeout(() => {
|
||||
setImmediate(() => void Promise.reject('first immediate'));
|
||||
setImmediate(() => {
|
||||
console.log('second immediate');
|
||||
process.nextTick(() => console.log('tick'));
|
||||
void Promise.reject('second immediate');
|
||||
});
|
||||
setImmediate(() => console.log('third immediate'));
|
||||
}, 30);
|
||||
@@ -0,0 +1,22 @@
|
||||
// Prototype getters named like String.prototype's HTML methods (bold,
|
||||
// italics, link, big): each read builds a callable that wraps its input.
|
||||
const wrap = (tag) => ({
|
||||
get() {
|
||||
const parent = this;
|
||||
const builder = (text) => builder.prefix(text);
|
||||
builder.prefix = (text) => parent.prefix(`<${tag}>` + text + `</${tag}>`);
|
||||
Object.setPrototypeOf(builder, proto);
|
||||
return builder;
|
||||
},
|
||||
});
|
||||
const proto = Object.defineProperties(() => {}, {
|
||||
bold: wrap("b"),
|
||||
italics: wrap("i"),
|
||||
link: wrap("a"),
|
||||
big: wrap("big"),
|
||||
red: wrap("red"),
|
||||
});
|
||||
const root = (text) => text;
|
||||
root.prefix = (text) => text;
|
||||
Object.setPrototypeOf(root, proto);
|
||||
export default root;
|
||||
@@ -0,0 +1,14 @@
|
||||
// A TypeScript caller reaching JavaScript builders whose member names
|
||||
// collide with String.prototype methods: the receiver is a function, so
|
||||
// each call reads the function's inherited getter, not the string method.
|
||||
import builder from "./builder.js";
|
||||
|
||||
// TypeScript infers no members for the builder's prototype getters.
|
||||
const tags: any = builder;
|
||||
|
||||
console.log(tags.bold("strong"));
|
||||
console.log(tags.red.bold("nested"));
|
||||
console.log(tags.italics.link("deep").length);
|
||||
console.log(tags.big.red.italics("three"));
|
||||
const chain = tags.link;
|
||||
console.log(chain.bold("from a binding"));
|
||||
@@ -0,0 +1,110 @@
|
||||
// A chalk-style builder: every style read through a function's runtime
|
||||
// prototype chain runs a getter that creates a new callable builder,
|
||||
// links it to a shared prototype with Object.setPrototypeOf, and caches
|
||||
// it on the receiver with Object.defineProperty.
|
||||
const GENERATOR = Symbol("GENERATOR");
|
||||
const STYLER = Symbol("STYLER");
|
||||
const IS_EMPTY = Symbol("IS_EMPTY");
|
||||
const codes = { red: [31, 39], green: [32, 39], bold: [1, 22], underline: [4, 24] };
|
||||
let getterRuns = 0;
|
||||
|
||||
const styles = Object.create(null);
|
||||
for (const [name, [open, close]] of Object.entries(codes)) {
|
||||
styles[name] = {
|
||||
get() {
|
||||
getterRuns++;
|
||||
const builder = createBuilder(this, createStyler(`<${open}>`, `</${close}>`, this[STYLER]), this[IS_EMPTY]);
|
||||
Object.defineProperty(this, name, { value: builder });
|
||||
return builder;
|
||||
},
|
||||
};
|
||||
}
|
||||
styles.visible = {
|
||||
get() {
|
||||
const builder = createBuilder(this, this[STYLER], true);
|
||||
Object.defineProperty(this, "visible", { value: builder });
|
||||
return builder;
|
||||
},
|
||||
};
|
||||
|
||||
const proto = Object.defineProperties(() => {}, {
|
||||
...styles,
|
||||
level: {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return this[GENERATOR].level;
|
||||
},
|
||||
set(level) {
|
||||
this[GENERATOR].level = level;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function createStyler(open, close, parent) {
|
||||
if (parent === undefined) return { open, close, openAll: open, closeAll: close, parent };
|
||||
return { open, close, openAll: parent.openAll + open, closeAll: close + parent.closeAll, parent };
|
||||
}
|
||||
|
||||
function createBuilder(self, styler, isEmpty) {
|
||||
const builder = (...args) => applyStyle(builder, args.length === 1 ? "" + args[0] : args.join(" "));
|
||||
Object.setPrototypeOf(builder, proto);
|
||||
builder[GENERATOR] = self;
|
||||
builder[STYLER] = styler;
|
||||
builder[IS_EMPTY] = isEmpty;
|
||||
return builder;
|
||||
}
|
||||
|
||||
function applyStyle(self, text) {
|
||||
if (self.level <= 0 || !text) return self[IS_EMPTY] ? "" : text;
|
||||
let styler = self[STYLER];
|
||||
if (styler === undefined) return text;
|
||||
const { openAll, closeAll } = styler;
|
||||
if (text.includes("</")) {
|
||||
for (; styler !== undefined; styler = styler.parent) text = replaceAll(text, styler.close, styler.close + styler.open);
|
||||
}
|
||||
return openAll + text + closeAll;
|
||||
}
|
||||
|
||||
function replaceAll(text, search, replacement) {
|
||||
let index = text.indexOf(search);
|
||||
if (index === -1) return text;
|
||||
let out = "";
|
||||
let end = 0;
|
||||
do {
|
||||
out += text.slice(end, index) + replacement;
|
||||
end = index + search.length;
|
||||
index = text.indexOf(search, end);
|
||||
} while (index !== -1);
|
||||
return out + text.slice(end);
|
||||
}
|
||||
|
||||
function createChalk(options) {
|
||||
const chalk = (...strings) => strings.join(" ");
|
||||
chalk.level = options && options.level !== undefined ? options.level : 1;
|
||||
Object.setPrototypeOf(chalk, createChalk.prototype);
|
||||
return chalk;
|
||||
}
|
||||
Object.setPrototypeOf(createChalk.prototype, Function.prototype);
|
||||
Object.defineProperties(createChalk.prototype, styles);
|
||||
|
||||
const chalk = createChalk();
|
||||
console.log(chalk.red("x"));
|
||||
console.log(chalk.red.bold("nested"));
|
||||
console.log(chalk.green("a " + chalk.red("b") + " c"));
|
||||
console.log(chalk.underline.green.bold("three", "args", 3));
|
||||
console.log(JSON.stringify(chalk.red("")), chalk.visible("v"), chalk.red.visible("rv"));
|
||||
console.log(chalk("plain", "call"), typeof chalk, typeof chalk.red, typeof chalk.red.bold);
|
||||
console.log(chalk.red === chalk.red, chalk.red.bold === chalk.red.bold, getterRuns);
|
||||
console.log(Object.getPrototypeOf(chalk) === createChalk.prototype);
|
||||
console.log(Object.getPrototypeOf(chalk.red) === proto, Object.getPrototypeOf(chalk.red.bold) === proto);
|
||||
console.log(Object.getPrototypeOf(createChalk.prototype) === Function.prototype);
|
||||
console.log(chalk.apply(null, ["via", "apply"]));
|
||||
console.log(Object.keys(chalk).join(","), Object.hasOwn(chalk, "red"), "red" in chalk);
|
||||
|
||||
const red = chalk.red;
|
||||
chalk.level = 0;
|
||||
console.log(red("off"), red.level, chalk.level, JSON.stringify(red.visible("hidden")));
|
||||
red.level = 2;
|
||||
console.log(chalk.level, chalk.green.level, red("on"));
|
||||
const quiet = createChalk({ level: 0 });
|
||||
console.log(quiet.red.bold("quiet"), quiet.level, chalk.level);
|
||||
@@ -0,0 +1,35 @@
|
||||
// Classes whose constructor returns a value: `new` answers that value.
|
||||
const make = (options = {}) => {
|
||||
const fn = (...parts) => parts.join(options.separator ?? " ");
|
||||
fn.level = options.level ?? 1;
|
||||
return fn;
|
||||
};
|
||||
|
||||
export class Joiner {
|
||||
constructor(options) {
|
||||
return make(options);
|
||||
}
|
||||
}
|
||||
|
||||
export class Record {
|
||||
constructor(name, size) {
|
||||
const value = { name, size, kind: "record" };
|
||||
if (size > 10) return { ...value, large: true };
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
let constructed = 0;
|
||||
export class Counted {
|
||||
constructor() {
|
||||
constructed++;
|
||||
return { id: constructed };
|
||||
}
|
||||
}
|
||||
|
||||
const j = new Joiner({ separator: "-", level: 3 });
|
||||
console.log(typeof j, j("a", "b", "c"), j.level);
|
||||
const plain = new Joiner();
|
||||
console.log(plain("x", "y"), plain.level);
|
||||
console.log(JSON.stringify(new Record("box", 4)), JSON.stringify(new Record("crate", 40)));
|
||||
console.log(new Counted().id, new Counted().id, constructed);
|
||||
@@ -0,0 +1,11 @@
|
||||
// The same classes constructed from TypeScript: the values come from the
|
||||
// constructors' returns.
|
||||
import { Counted, Joiner, Record } from "./factory.js";
|
||||
|
||||
// TypeScript types `new` by the class; the values are what the constructors return.
|
||||
const joiner: any = new Joiner({ separator: "+" });
|
||||
console.log(joiner("1", "2"));
|
||||
const record = new Record("tube", 11);
|
||||
console.log(JSON.stringify(record));
|
||||
const counted: any = new Counted();
|
||||
console.log(counted.id);
|
||||
@@ -0,0 +1,17 @@
|
||||
// Function.prototype as a value: a callable answering undefined, with an
|
||||
// empty name and length 0, usable as a prototype-chain link.
|
||||
const fp = Function.prototype;
|
||||
console.log(typeof fp, fp === Function.prototype, JSON.stringify(fp.name), fp.length);
|
||||
console.log(fp(), fp(1, 2, 3));
|
||||
|
||||
function Base() {}
|
||||
Object.setPrototypeOf(Base.prototype, Function.prototype);
|
||||
console.log(Object.getPrototypeOf(Base.prototype) === Function.prototype);
|
||||
|
||||
const callable = (...args) => args.length;
|
||||
Object.setPrototypeOf(callable, Base.prototype);
|
||||
console.log(callable(1, 2), Object.getPrototypeOf(callable) === Base.prototype);
|
||||
console.log(callable.apply(null, [1, 2, 3]), callable.call(null, 1));
|
||||
|
||||
const plain = Object.setPrototypeOf({ own: 1 }, Function.prototype);
|
||||
console.log(typeof plain, plain.own, Object.getPrototypeOf(plain) === Function.prototype);
|
||||
@@ -0,0 +1,8 @@
|
||||
// Number.parseInt / Number.parseFloat over checked-dynamic values apply
|
||||
// the parsers' own ToString, as supports-color does with FORCE_COLOR.
|
||||
const env = JSON.parse('{"FORCE_COLOR":"2","DEPTH":" 3.5px","EMPTY":"","HEX":"ff"}');
|
||||
console.log(Number.parseInt(env.FORCE_COLOR, 10), Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3));
|
||||
console.log(Number.parseInt(env.MISSING, 10), Number.parseInt(env.EMPTY, 10), Number.parseInt(env.HEX, 16));
|
||||
console.log(Number.parseFloat(env.DEPTH), Number.parseFloat(env.MISSING));
|
||||
const values = JSON.parse("[12, true, null]");
|
||||
console.log(Number.parseInt(values[0], 10), Number.parseInt(values[1], 36), Number.parseInt(values[2], 36));
|
||||
@@ -0,0 +1,12 @@
|
||||
// Data members of the process object taken off by destructuring.
|
||||
import process from "node:process";
|
||||
|
||||
const { env, argv, platform } = process;
|
||||
console.log(env.SCRIPTC_TEST_ENV, "SCRIPTC_TEST_ENV" in env, "SCRIPTC_NOT_SET_ANYWHERE" in env);
|
||||
console.log(Array.isArray(argv), typeof argv[0], platform === process.platform);
|
||||
|
||||
function readFlags() {
|
||||
const { env: environment, pid } = process;
|
||||
return [typeof environment.SCRIPTC_TEST_ENV, pid === process.pid];
|
||||
}
|
||||
console.log(readFlags().join(" "));
|
||||
@@ -0,0 +1,42 @@
|
||||
// An object literal the program reshapes at run time (runtime-keyed
|
||||
// writes, defineProperty, delete): every read sees the live object, not
|
||||
// the layout TypeScript inferred for the literal (ansi-styles'
|
||||
// assembleStyles shape).
|
||||
const styles = {
|
||||
modifier: { reset: [0, 0], bold: [1, 22] },
|
||||
color: { red: [31, 39], green: [32, 39] },
|
||||
};
|
||||
|
||||
function assemble() {
|
||||
const codes = new Map();
|
||||
for (const [groupName, group] of Object.entries(styles)) {
|
||||
for (const [styleName, style] of Object.entries(group)) {
|
||||
styles[styleName] = { open: `<${style[0]}>`, close: `</${style[1]}>` };
|
||||
group[styleName] = styles[styleName];
|
||||
codes.set(style[0], style[1]);
|
||||
}
|
||||
Object.defineProperty(styles, groupName, { value: group, enumerable: false });
|
||||
}
|
||||
Object.defineProperty(styles, "codes", { value: codes, enumerable: false });
|
||||
styles.color.close = "</color>";
|
||||
styles.color.ansi = (code) => `[${code}]`;
|
||||
Object.defineProperties(styles, {
|
||||
double: { value: (n) => n * 2, enumerable: false },
|
||||
});
|
||||
return styles;
|
||||
}
|
||||
|
||||
const ansi = assemble();
|
||||
console.log(Object.keys(ansi).join(","));
|
||||
console.log(ansi.red.open, ansi.color.red.close, ansi.color.close, ansi.color.ansi(5));
|
||||
console.log(ansi.codes.get(31), ansi.codes.size, ansi.double(21));
|
||||
console.log(ansi.modifier.bold === ansi.bold, Object.keys(ansi.color).join(","));
|
||||
for (const [name, style] of Object.entries(ansi)) console.log(name, style.open, style.close);
|
||||
|
||||
const table = { a: 1, b: 2, c: 3 };
|
||||
const drop = (key) => {
|
||||
delete table[key];
|
||||
};
|
||||
drop("b");
|
||||
table["d" + 1] = 4;
|
||||
console.log(JSON.stringify(table), Object.keys(table).length, "b" in table);
|
||||
@@ -0,0 +1,31 @@
|
||||
// `&&` and `||` whose left operand is an object, array or function value:
|
||||
// every object is truthy, so `o && x` evaluates `o` and answers `x`, and
|
||||
// `o || x` answers `o` without evaluating `x`.
|
||||
interface Stream {
|
||||
isTTY: boolean;
|
||||
columns: number;
|
||||
}
|
||||
let reads = 0;
|
||||
const stream = (): Stream => {
|
||||
reads++;
|
||||
return { isTTY: reads % 2 === 1, columns: 80 };
|
||||
};
|
||||
function tty(s: Stream): boolean {
|
||||
return s && s.isTTY;
|
||||
}
|
||||
console.log(tty({ isTTY: true, columns: 1 }), tty({ isTTY: false, columns: 2 }));
|
||||
const width = stream() && stream().columns;
|
||||
console.log(width, reads);
|
||||
const list = [3, 1, 2];
|
||||
const first = list && list.length;
|
||||
console.log(first);
|
||||
const fn = (n: number): number => n * 2;
|
||||
const doubled = fn && fn(21);
|
||||
console.log(doubled);
|
||||
let evaluated = false;
|
||||
const fallback = (): Stream => {
|
||||
evaluated = true;
|
||||
return { isTTY: false, columns: 0 };
|
||||
};
|
||||
const chosen = stream() || fallback();
|
||||
console.log(chosen.columns, evaluated, reads);
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// chalk 5 (the real package, pinned in node_modules): the callable
|
||||
// builder whose styles are prototype getters on a function chain, every
|
||||
// color model, nested styles, chalk.level and new Chalk({ level }) at all
|
||||
// four levels. chalk.test.ts runs the same program under FORCE_COLOR and
|
||||
// the --color flags.
|
||||
import chalk, { Chalk, chalkStderr, supportsColor, modifierNames, foregroundColorNames, backgroundColorNames, colorNames } from "chalk";
|
||||
|
||||
const show = (label: string, value: string): void => {
|
||||
console.log(label + " " + JSON.stringify(value));
|
||||
};
|
||||
|
||||
console.log("default level " + String(chalk.level) + " stderr " + String(chalkStderr.level));
|
||||
console.log("supportsColor " + JSON.stringify(supportsColor));
|
||||
console.log(modifierNames.join(","));
|
||||
console.log(foregroundColorNames.length + " " + backgroundColorNames.length + " " + colorNames.length);
|
||||
|
||||
show("red", chalk.red("red"));
|
||||
show("red.bold", chalk.red.bold("bold red"));
|
||||
show("nested", chalk.blue("a " + chalk.red("b") + " c"));
|
||||
show("multi-arg", chalk.green("one", "two", 3));
|
||||
show("number", chalk.yellow(42 as unknown as string));
|
||||
show("empty", chalk.red(""));
|
||||
show("newline", chalk.magenta.underline("line1\nline2\r\nline3"));
|
||||
show("bg", chalk.bgCyan.black("bg"));
|
||||
show("bright", chalk.redBright.bgBlueBright("bright"));
|
||||
show("modifiers", chalk.italic.inverse.strikethrough.overline("mods"));
|
||||
show("hidden-visible", chalk.hidden("h") + "|" + chalk.visible("v"));
|
||||
show("rgb", chalk.rgb(10, 20, 30)("rgb"));
|
||||
show("hex", chalk.hex("#ff8800").bold("hex"));
|
||||
show("hex-short", chalk.bgHex("#0f0")("bg hex"));
|
||||
show("ansi256", chalk.ansi256(201)("ansi256"));
|
||||
show("bgAnsi256", chalk.bgAnsi256(17).white("bgAnsi256"));
|
||||
show("callable", chalk("plain", "args"));
|
||||
show("gray-grey", chalk.gray("x") + chalk.grey("y"));
|
||||
|
||||
for (const level of [0, 1, 2, 3] as const) {
|
||||
const c = new Chalk({ level });
|
||||
show(`L${level} red.bold`, c.red.bold("x"));
|
||||
show(`L${level} hex`, c.hex("#123456")("x"));
|
||||
show(`L${level} rgb`, c.rgb(255, 255, 255).bgRgb(0, 0, 0)("x"));
|
||||
show(`L${level} ansi256`, c.ansi256(100).bgAnsi256(200)("x"));
|
||||
show(`L${level} visible`, c.visible.red("v"));
|
||||
show(`L${level} nested`, c.green("g" + c.yellow("y") + "g"));
|
||||
}
|
||||
|
||||
const mutable = new Chalk({ level: 3 });
|
||||
const red = mutable.red;
|
||||
show("before", red("r"));
|
||||
mutable.level = 0;
|
||||
show("after level 0", red("r") + "|" + mutable.blue("b"));
|
||||
console.log("level via builder " + String(red.level));
|
||||
red.level = 2;
|
||||
console.log("shared level " + String(mutable.level));
|
||||
show("restored", mutable.cyan("c"));
|
||||
|
||||
try {
|
||||
new Chalk({ level: 5 as 0 });
|
||||
} catch (error) {
|
||||
console.log("error " + (error as Error).message);
|
||||
}
|
||||
|
||||
chalk.level = 1;
|
||||
show("global after set", chalk.red.bold("x"));
|
||||
console.log("global level " + String(chalk.level));
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "chalk",
|
||||
"version": "5.6.2",
|
||||
"description": "Terminal string styling done right",
|
||||
"license": "MIT",
|
||||
"repository": "chalk/chalk",
|
||||
"funding": "https://github.com/chalk/chalk?sponsor=1",
|
||||
"type": "module",
|
||||
"main": "./source/index.js",
|
||||
"exports": "./source/index.js",
|
||||
"imports": {
|
||||
"#ansi-styles": "./source/vendor/ansi-styles/index.js",
|
||||
"#supports-color": {
|
||||
"node": "./source/vendor/supports-color/index.js",
|
||||
"default": "./source/vendor/supports-color/browser.js"
|
||||
}
|
||||
},
|
||||
"types": "./source/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "xo && c8 ava && tsd",
|
||||
"bench": "matcha benchmark.js"
|
||||
},
|
||||
"files": [
|
||||
"source",
|
||||
"!source/index.test-d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"color",
|
||||
"colour",
|
||||
"colors",
|
||||
"terminal",
|
||||
"console",
|
||||
"cli",
|
||||
"string",
|
||||
"ansi",
|
||||
"style",
|
||||
"styles",
|
||||
"tty",
|
||||
"formatting",
|
||||
"rgb",
|
||||
"256",
|
||||
"shell",
|
||||
"xterm",
|
||||
"log",
|
||||
"logging",
|
||||
"command-line",
|
||||
"text"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.10",
|
||||
"ava": "^3.15.0",
|
||||
"c8": "^7.10.0",
|
||||
"color-convert": "^2.0.1",
|
||||
"execa": "^6.0.0",
|
||||
"log-update": "^5.0.0",
|
||||
"matcha": "^0.7.0",
|
||||
"tsd": "^0.19.0",
|
||||
"xo": "^0.57.0",
|
||||
"yoctodelay": "^2.0.0"
|
||||
},
|
||||
"xo": {
|
||||
"rules": {
|
||||
"unicorn/prefer-string-slice": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "off",
|
||||
"@typescript-eslint/consistent-type-exports": "off",
|
||||
"@typescript-eslint/consistent-type-definitions": "off",
|
||||
"unicorn/expiring-todo-comments": "off"
|
||||
}
|
||||
},
|
||||
"c8": {
|
||||
"reporter": [
|
||||
"text",
|
||||
"lcov"
|
||||
],
|
||||
"exclude": [
|
||||
"source/vendor"
|
||||
]
|
||||
}
|
||||
}
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
// TODO: Make it this when TS suports that.
|
||||
// import {ModifierName, ForegroundColor, BackgroundColor, ColorName} from '#ansi-styles';
|
||||
// import {ColorInfo, ColorSupportLevel} from '#supports-color';
|
||||
import {
|
||||
ModifierName,
|
||||
ForegroundColorName,
|
||||
BackgroundColorName,
|
||||
ColorName,
|
||||
} from './vendor/ansi-styles/index.js';
|
||||
import {ColorInfo, ColorSupportLevel} from './vendor/supports-color/index.js';
|
||||
|
||||
export interface Options {
|
||||
/**
|
||||
Specify the color support for Chalk.
|
||||
|
||||
By default, color support is automatically detected based on the environment.
|
||||
|
||||
Levels:
|
||||
- `0` - All colors disabled.
|
||||
- `1` - Basic 16 colors support.
|
||||
- `2` - ANSI 256 colors support.
|
||||
- `3` - Truecolor 16 million colors support.
|
||||
*/
|
||||
readonly level?: ColorSupportLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
Return a new Chalk instance.
|
||||
*/
|
||||
export const Chalk: new (options?: Options) => ChalkInstance; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
|
||||
export interface ChalkInstance {
|
||||
(...text: unknown[]): string;
|
||||
|
||||
/**
|
||||
The color support for Chalk.
|
||||
|
||||
By default, color support is automatically detected based on the environment.
|
||||
|
||||
Levels:
|
||||
- `0` - All colors disabled.
|
||||
- `1` - Basic 16 colors support.
|
||||
- `2` - ANSI 256 colors support.
|
||||
- `3` - Truecolor 16 million colors support.
|
||||
*/
|
||||
level: ColorSupportLevel;
|
||||
|
||||
/**
|
||||
Use RGB values to set text color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.rgb(222, 173, 237);
|
||||
```
|
||||
*/
|
||||
rgb: (red: number, green: number, blue: number) => this;
|
||||
|
||||
/**
|
||||
Use HEX value to set text color.
|
||||
|
||||
@param color - Hexadecimal value representing the desired color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.hex('#DEADED');
|
||||
```
|
||||
*/
|
||||
hex: (color: string) => this;
|
||||
|
||||
/**
|
||||
Use an [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.ansi256(201);
|
||||
```
|
||||
*/
|
||||
ansi256: (index: number) => this;
|
||||
|
||||
/**
|
||||
Use RGB values to set background color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.bgRgb(222, 173, 237);
|
||||
```
|
||||
*/
|
||||
bgRgb: (red: number, green: number, blue: number) => this;
|
||||
|
||||
/**
|
||||
Use HEX value to set background color.
|
||||
|
||||
@param color - Hexadecimal value representing the desired color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.bgHex('#DEADED');
|
||||
```
|
||||
*/
|
||||
bgHex: (color: string) => this;
|
||||
|
||||
/**
|
||||
Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set background color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.bgAnsi256(201);
|
||||
```
|
||||
*/
|
||||
bgAnsi256: (index: number) => this;
|
||||
|
||||
/**
|
||||
Modifier: Reset the current style.
|
||||
*/
|
||||
readonly reset: this;
|
||||
|
||||
/**
|
||||
Modifier: Make the text bold.
|
||||
*/
|
||||
readonly bold: this;
|
||||
|
||||
/**
|
||||
Modifier: Make the text have lower opacity.
|
||||
*/
|
||||
readonly dim: this;
|
||||
|
||||
/**
|
||||
Modifier: Make the text italic. *(Not widely supported)*
|
||||
*/
|
||||
readonly italic: this;
|
||||
|
||||
/**
|
||||
Modifier: Put a horizontal line below the text. *(Not widely supported)*
|
||||
*/
|
||||
readonly underline: this;
|
||||
|
||||
/**
|
||||
Modifier: Put a horizontal line above the text. *(Not widely supported)*
|
||||
*/
|
||||
readonly overline: this;
|
||||
|
||||
/**
|
||||
Modifier: Invert background and foreground colors.
|
||||
*/
|
||||
readonly inverse: this;
|
||||
|
||||
/**
|
||||
Modifier: Print the text but make it invisible.
|
||||
*/
|
||||
readonly hidden: this;
|
||||
|
||||
/**
|
||||
Modifier: Puts a horizontal line through the center of the text. *(Not widely supported)*
|
||||
*/
|
||||
readonly strikethrough: this;
|
||||
|
||||
/**
|
||||
Modifier: Print the text only when Chalk has a color level above zero.
|
||||
|
||||
Can be useful for things that are purely cosmetic.
|
||||
*/
|
||||
readonly visible: this;
|
||||
|
||||
readonly black: this;
|
||||
readonly red: this;
|
||||
readonly green: this;
|
||||
readonly yellow: this;
|
||||
readonly blue: this;
|
||||
readonly magenta: this;
|
||||
readonly cyan: this;
|
||||
readonly white: this;
|
||||
|
||||
/*
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly gray: this;
|
||||
|
||||
/*
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly grey: this;
|
||||
|
||||
readonly blackBright: this;
|
||||
readonly redBright: this;
|
||||
readonly greenBright: this;
|
||||
readonly yellowBright: this;
|
||||
readonly blueBright: this;
|
||||
readonly magentaBright: this;
|
||||
readonly cyanBright: this;
|
||||
readonly whiteBright: this;
|
||||
|
||||
readonly bgBlack: this;
|
||||
readonly bgRed: this;
|
||||
readonly bgGreen: this;
|
||||
readonly bgYellow: this;
|
||||
readonly bgBlue: this;
|
||||
readonly bgMagenta: this;
|
||||
readonly bgCyan: this;
|
||||
readonly bgWhite: this;
|
||||
|
||||
/*
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGray: this;
|
||||
|
||||
/*
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGrey: this;
|
||||
|
||||
readonly bgBlackBright: this;
|
||||
readonly bgRedBright: this;
|
||||
readonly bgGreenBright: this;
|
||||
readonly bgYellowBright: this;
|
||||
readonly bgBlueBright: this;
|
||||
readonly bgMagentaBright: this;
|
||||
readonly bgCyanBright: this;
|
||||
readonly bgWhiteBright: this;
|
||||
}
|
||||
|
||||
/**
|
||||
Main Chalk object that allows to chain styles together.
|
||||
|
||||
Call the last one as a method with a string argument.
|
||||
|
||||
Order doesn't matter, and later styles take precedent in case of a conflict.
|
||||
|
||||
This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`.
|
||||
*/
|
||||
declare const chalk: ChalkInstance;
|
||||
|
||||
export const supportsColor: ColorInfo;
|
||||
|
||||
export const chalkStderr: typeof chalk;
|
||||
export const supportsColorStderr: typeof supportsColor;
|
||||
|
||||
export {
|
||||
ModifierName, ForegroundColorName, BackgroundColorName, ColorName,
|
||||
modifierNames, foregroundColorNames, backgroundColorNames, colorNames,
|
||||
// } from '#ansi-styles';
|
||||
} from './vendor/ansi-styles/index.js';
|
||||
|
||||
export {
|
||||
ColorInfo,
|
||||
ColorSupport,
|
||||
ColorSupportLevel,
|
||||
// } from '#supports-color';
|
||||
} from './vendor/supports-color/index.js';
|
||||
|
||||
// TODO: Remove these aliases in the next major version
|
||||
/**
|
||||
@deprecated Use `ModifierName` instead.
|
||||
|
||||
Basic modifier names.
|
||||
*/
|
||||
export type Modifiers = ModifierName;
|
||||
|
||||
/**
|
||||
@deprecated Use `ForegroundColorName` instead.
|
||||
|
||||
Basic foreground color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ForegroundColor = ForegroundColorName;
|
||||
|
||||
/**
|
||||
@deprecated Use `BackgroundColorName` instead.
|
||||
|
||||
Basic background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type BackgroundColor = BackgroundColorName;
|
||||
|
||||
/**
|
||||
@deprecated Use `ColorName` instead.
|
||||
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type Color = ColorName;
|
||||
|
||||
/**
|
||||
@deprecated Use `modifierNames` instead.
|
||||
|
||||
Basic modifier names.
|
||||
*/
|
||||
export const modifiers: readonly Modifiers[];
|
||||
|
||||
/**
|
||||
@deprecated Use `foregroundColorNames` instead.
|
||||
|
||||
Basic foreground color names.
|
||||
*/
|
||||
export const foregroundColors: readonly ForegroundColor[];
|
||||
|
||||
/**
|
||||
@deprecated Use `backgroundColorNames` instead.
|
||||
|
||||
Basic background color names.
|
||||
*/
|
||||
export const backgroundColors: readonly BackgroundColor[];
|
||||
|
||||
/**
|
||||
@deprecated Use `colorNames` instead.
|
||||
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
*/
|
||||
export const colors: readonly Color[];
|
||||
|
||||
export default chalk;
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
import ansiStyles from '#ansi-styles';
|
||||
import supportsColor from '#supports-color';
|
||||
import { // eslint-disable-line import/order
|
||||
stringReplaceAll,
|
||||
stringEncaseCRLFWithFirstIndex,
|
||||
} from './utilities.js';
|
||||
|
||||
const {stdout: stdoutColor, stderr: stderrColor} = supportsColor;
|
||||
|
||||
const GENERATOR = Symbol('GENERATOR');
|
||||
const STYLER = Symbol('STYLER');
|
||||
const IS_EMPTY = Symbol('IS_EMPTY');
|
||||
|
||||
// `supportsColor.level` → `ansiStyles.color[name]` mapping
|
||||
const levelMapping = [
|
||||
'ansi',
|
||||
'ansi',
|
||||
'ansi256',
|
||||
'ansi16m',
|
||||
];
|
||||
|
||||
const styles = Object.create(null);
|
||||
|
||||
const applyOptions = (object, options = {}) => {
|
||||
if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
|
||||
throw new Error('The `level` option should be an integer from 0 to 3');
|
||||
}
|
||||
|
||||
// Detect level if not set manually
|
||||
const colorLevel = stdoutColor ? stdoutColor.level : 0;
|
||||
object.level = options.level === undefined ? colorLevel : options.level;
|
||||
};
|
||||
|
||||
export class Chalk {
|
||||
constructor(options) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return chalkFactory(options);
|
||||
}
|
||||
}
|
||||
|
||||
const chalkFactory = options => {
|
||||
const chalk = (...strings) => strings.join(' ');
|
||||
applyOptions(chalk, options);
|
||||
|
||||
Object.setPrototypeOf(chalk, createChalk.prototype);
|
||||
|
||||
return chalk;
|
||||
};
|
||||
|
||||
function createChalk(options) {
|
||||
return chalkFactory(options);
|
||||
}
|
||||
|
||||
Object.setPrototypeOf(createChalk.prototype, Function.prototype);
|
||||
|
||||
for (const [styleName, style] of Object.entries(ansiStyles)) {
|
||||
styles[styleName] = {
|
||||
get() {
|
||||
const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
|
||||
Object.defineProperty(this, styleName, {value: builder});
|
||||
return builder;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
styles.visible = {
|
||||
get() {
|
||||
const builder = createBuilder(this, this[STYLER], true);
|
||||
Object.defineProperty(this, 'visible', {value: builder});
|
||||
return builder;
|
||||
},
|
||||
};
|
||||
|
||||
const getModelAnsi = (model, level, type, ...arguments_) => {
|
||||
if (model === 'rgb') {
|
||||
if (level === 'ansi16m') {
|
||||
return ansiStyles[type].ansi16m(...arguments_);
|
||||
}
|
||||
|
||||
if (level === 'ansi256') {
|
||||
return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_));
|
||||
}
|
||||
|
||||
return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_));
|
||||
}
|
||||
|
||||
if (model === 'hex') {
|
||||
return getModelAnsi('rgb', level, type, ...ansiStyles.hexToRgb(...arguments_));
|
||||
}
|
||||
|
||||
return ansiStyles[type][model](...arguments_);
|
||||
};
|
||||
|
||||
const usedModels = ['rgb', 'hex', 'ansi256'];
|
||||
|
||||
for (const model of usedModels) {
|
||||
styles[model] = {
|
||||
get() {
|
||||
const {level} = this;
|
||||
return function (...arguments_) {
|
||||
const styler = createStyler(getModelAnsi(model, levelMapping[level], 'color', ...arguments_), ansiStyles.color.close, this[STYLER]);
|
||||
return createBuilder(this, styler, this[IS_EMPTY]);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1);
|
||||
styles[bgModel] = {
|
||||
get() {
|
||||
const {level} = this;
|
||||
return function (...arguments_) {
|
||||
const styler = createStyler(getModelAnsi(model, levelMapping[level], 'bgColor', ...arguments_), ansiStyles.bgColor.close, this[STYLER]);
|
||||
return createBuilder(this, styler, this[IS_EMPTY]);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const proto = Object.defineProperties(() => {}, {
|
||||
...styles,
|
||||
level: {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return this[GENERATOR].level;
|
||||
},
|
||||
set(level) {
|
||||
this[GENERATOR].level = level;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createStyler = (open, close, parent) => {
|
||||
let openAll;
|
||||
let closeAll;
|
||||
if (parent === undefined) {
|
||||
openAll = open;
|
||||
closeAll = close;
|
||||
} else {
|
||||
openAll = parent.openAll + open;
|
||||
closeAll = close + parent.closeAll;
|
||||
}
|
||||
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
openAll,
|
||||
closeAll,
|
||||
parent,
|
||||
};
|
||||
};
|
||||
|
||||
const createBuilder = (self, _styler, _isEmpty) => {
|
||||
// Single argument is hot path, implicit coercion is faster than anything
|
||||
// eslint-disable-next-line no-implicit-coercion
|
||||
const builder = (...arguments_) => applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' '));
|
||||
|
||||
// We alter the prototype because we must return a function, but there is
|
||||
// no way to create a function with a different prototype
|
||||
Object.setPrototypeOf(builder, proto);
|
||||
|
||||
builder[GENERATOR] = self;
|
||||
builder[STYLER] = _styler;
|
||||
builder[IS_EMPTY] = _isEmpty;
|
||||
|
||||
return builder;
|
||||
};
|
||||
|
||||
const applyStyle = (self, string) => {
|
||||
if (self.level <= 0 || !string) {
|
||||
return self[IS_EMPTY] ? '' : string;
|
||||
}
|
||||
|
||||
let styler = self[STYLER];
|
||||
|
||||
if (styler === undefined) {
|
||||
return string;
|
||||
}
|
||||
|
||||
const {openAll, closeAll} = styler;
|
||||
if (string.includes('\u001B')) {
|
||||
while (styler !== undefined) {
|
||||
// Replace any instances already present with a re-opening code
|
||||
// otherwise only the part of the string until said closing code
|
||||
// will be colored, and the rest will simply be 'plain'.
|
||||
string = stringReplaceAll(string, styler.close, styler.open);
|
||||
|
||||
styler = styler.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// We can move both next actions out of loop, because remaining actions in loop won't have
|
||||
// any/visible effect on parts we add here. Close the styling before a linebreak and reopen
|
||||
// after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92
|
||||
const lfIndex = string.indexOf('\n');
|
||||
if (lfIndex !== -1) {
|
||||
string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
|
||||
}
|
||||
|
||||
return openAll + string + closeAll;
|
||||
};
|
||||
|
||||
Object.defineProperties(createChalk.prototype, styles);
|
||||
|
||||
const chalk = createChalk();
|
||||
export const chalkStderr = createChalk({level: stderrColor ? stderrColor.level : 0});
|
||||
|
||||
export {
|
||||
modifierNames,
|
||||
foregroundColorNames,
|
||||
backgroundColorNames,
|
||||
colorNames,
|
||||
|
||||
// TODO: Remove these aliases in the next major version
|
||||
modifierNames as modifiers,
|
||||
foregroundColorNames as foregroundColors,
|
||||
backgroundColorNames as backgroundColors,
|
||||
colorNames as colors,
|
||||
} from './vendor/ansi-styles/index.js';
|
||||
|
||||
export {
|
||||
stdoutColor as supportsColor,
|
||||
stderrColor as supportsColorStderr,
|
||||
};
|
||||
|
||||
export default chalk;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// TODO: When targeting Node.js 16, use `String.prototype.replaceAll`.
|
||||
export function stringReplaceAll(string, substring, replacer) {
|
||||
let index = string.indexOf(substring);
|
||||
if (index === -1) {
|
||||
return string;
|
||||
}
|
||||
|
||||
const substringLength = substring.length;
|
||||
let endIndex = 0;
|
||||
let returnValue = '';
|
||||
do {
|
||||
returnValue += string.slice(endIndex, index) + substring + replacer;
|
||||
endIndex = index + substringLength;
|
||||
index = string.indexOf(substring, endIndex);
|
||||
} while (index !== -1);
|
||||
|
||||
returnValue += string.slice(endIndex);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
export function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
||||
let endIndex = 0;
|
||||
let returnValue = '';
|
||||
do {
|
||||
const gotCR = string[index - 1] === '\r';
|
||||
returnValue += string.slice(endIndex, (gotCR ? index - 1 : index)) + prefix + (gotCR ? '\r\n' : '\n') + postfix;
|
||||
endIndex = index + 1;
|
||||
index = string.indexOf('\n', endIndex);
|
||||
} while (index !== -1);
|
||||
|
||||
returnValue += string.slice(endIndex);
|
||||
return returnValue;
|
||||
}
|
||||
Generated
Vendored
+236
@@ -0,0 +1,236 @@
|
||||
export interface CSPair { // eslint-disable-line @typescript-eslint/naming-convention
|
||||
/**
|
||||
The ANSI terminal control sequence for starting this style.
|
||||
*/
|
||||
readonly open: string;
|
||||
|
||||
/**
|
||||
The ANSI terminal control sequence for ending this style.
|
||||
*/
|
||||
readonly close: string;
|
||||
}
|
||||
|
||||
export interface ColorBase {
|
||||
/**
|
||||
The ANSI terminal control sequence for ending this color.
|
||||
*/
|
||||
readonly close: string;
|
||||
|
||||
ansi(code: number): string;
|
||||
|
||||
ansi256(code: number): string;
|
||||
|
||||
ansi16m(red: number, green: number, blue: number): string;
|
||||
}
|
||||
|
||||
export interface Modifier {
|
||||
/**
|
||||
Resets the current color chain.
|
||||
*/
|
||||
readonly reset: CSPair;
|
||||
|
||||
/**
|
||||
Make text bold.
|
||||
*/
|
||||
readonly bold: CSPair;
|
||||
|
||||
/**
|
||||
Emitting only a small amount of light.
|
||||
*/
|
||||
readonly dim: CSPair;
|
||||
|
||||
/**
|
||||
Make text italic. (Not widely supported)
|
||||
*/
|
||||
readonly italic: CSPair;
|
||||
|
||||
/**
|
||||
Make text underline. (Not widely supported)
|
||||
*/
|
||||
readonly underline: CSPair;
|
||||
|
||||
/**
|
||||
Make text overline.
|
||||
|
||||
Supported on VTE-based terminals, the GNOME terminal, mintty, and Git Bash.
|
||||
*/
|
||||
readonly overline: CSPair;
|
||||
|
||||
/**
|
||||
Inverse background and foreground colors.
|
||||
*/
|
||||
readonly inverse: CSPair;
|
||||
|
||||
/**
|
||||
Prints the text, but makes it invisible.
|
||||
*/
|
||||
readonly hidden: CSPair;
|
||||
|
||||
/**
|
||||
Puts a horizontal line through the center of the text. (Not widely supported)
|
||||
*/
|
||||
readonly strikethrough: CSPair;
|
||||
}
|
||||
|
||||
export interface ForegroundColor {
|
||||
readonly black: CSPair;
|
||||
readonly red: CSPair;
|
||||
readonly green: CSPair;
|
||||
readonly yellow: CSPair;
|
||||
readonly blue: CSPair;
|
||||
readonly cyan: CSPair;
|
||||
readonly magenta: CSPair;
|
||||
readonly white: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly gray: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly grey: CSPair;
|
||||
|
||||
readonly blackBright: CSPair;
|
||||
readonly redBright: CSPair;
|
||||
readonly greenBright: CSPair;
|
||||
readonly yellowBright: CSPair;
|
||||
readonly blueBright: CSPair;
|
||||
readonly cyanBright: CSPair;
|
||||
readonly magentaBright: CSPair;
|
||||
readonly whiteBright: CSPair;
|
||||
}
|
||||
|
||||
export interface BackgroundColor {
|
||||
readonly bgBlack: CSPair;
|
||||
readonly bgRed: CSPair;
|
||||
readonly bgGreen: CSPair;
|
||||
readonly bgYellow: CSPair;
|
||||
readonly bgBlue: CSPair;
|
||||
readonly bgCyan: CSPair;
|
||||
readonly bgMagenta: CSPair;
|
||||
readonly bgWhite: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGray: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGrey: CSPair;
|
||||
|
||||
readonly bgBlackBright: CSPair;
|
||||
readonly bgRedBright: CSPair;
|
||||
readonly bgGreenBright: CSPair;
|
||||
readonly bgYellowBright: CSPair;
|
||||
readonly bgBlueBright: CSPair;
|
||||
readonly bgCyanBright: CSPair;
|
||||
readonly bgMagentaBright: CSPair;
|
||||
readonly bgWhiteBright: CSPair;
|
||||
}
|
||||
|
||||
export interface ConvertColor {
|
||||
/**
|
||||
Convert from the RGB color space to the ANSI 256 color space.
|
||||
|
||||
@param red - (`0...255`)
|
||||
@param green - (`0...255`)
|
||||
@param blue - (`0...255`)
|
||||
*/
|
||||
rgbToAnsi256(red: number, green: number, blue: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the RGB color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToRgb(hex: string): [red: number, green: number, blue: number];
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the ANSI 256 color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToAnsi256(hex: string): number;
|
||||
|
||||
/**
|
||||
Convert from the ANSI 256 color space to the ANSI 16 color space.
|
||||
|
||||
@param code - A number representing the ANSI 256 color.
|
||||
*/
|
||||
ansi256ToAnsi(code: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB color space to the ANSI 16 color space.
|
||||
|
||||
@param red - (`0...255`)
|
||||
@param green - (`0...255`)
|
||||
@param blue - (`0...255`)
|
||||
*/
|
||||
rgbToAnsi(red: number, green: number, blue: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the ANSI 16 color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToAnsi(hex: string): number;
|
||||
}
|
||||
|
||||
/**
|
||||
Basic modifier names.
|
||||
*/
|
||||
export type ModifierName = keyof Modifier;
|
||||
|
||||
/**
|
||||
Basic foreground color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ForegroundColorName = keyof ForegroundColor;
|
||||
|
||||
/**
|
||||
Basic background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type BackgroundColorName = keyof BackgroundColor;
|
||||
|
||||
/**
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ColorName = ForegroundColorName | BackgroundColorName;
|
||||
|
||||
/**
|
||||
Basic modifier names.
|
||||
*/
|
||||
export const modifierNames: readonly ModifierName[];
|
||||
|
||||
/**
|
||||
Basic foreground color names.
|
||||
*/
|
||||
export const foregroundColorNames: readonly ForegroundColorName[];
|
||||
|
||||
/**
|
||||
Basic background color names.
|
||||
*/
|
||||
export const backgroundColorNames: readonly BackgroundColorName[];
|
||||
|
||||
/*
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
*/
|
||||
export const colorNames: readonly ColorName[];
|
||||
|
||||
declare const ansiStyles: {
|
||||
readonly modifier: Modifier;
|
||||
readonly color: ColorBase & ForegroundColor;
|
||||
readonly bgColor: ColorBase & BackgroundColor;
|
||||
readonly codes: ReadonlyMap<number, number>;
|
||||
} & ForegroundColor & BackgroundColor & Modifier & ConvertColor;
|
||||
|
||||
export default ansiStyles;
|
||||
Generated
Vendored
+223
@@ -0,0 +1,223 @@
|
||||
const ANSI_BACKGROUND_OFFSET = 10;
|
||||
|
||||
const wrapAnsi16 = (offset = 0) => code => `\u001B[${code + offset}m`;
|
||||
|
||||
const wrapAnsi256 = (offset = 0) => code => `\u001B[${38 + offset};5;${code}m`;
|
||||
|
||||
const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`;
|
||||
|
||||
const styles = {
|
||||
modifier: {
|
||||
reset: [0, 0],
|
||||
// 21 isn't widely supported and 22 does the same thing
|
||||
bold: [1, 22],
|
||||
dim: [2, 22],
|
||||
italic: [3, 23],
|
||||
underline: [4, 24],
|
||||
overline: [53, 55],
|
||||
inverse: [7, 27],
|
||||
hidden: [8, 28],
|
||||
strikethrough: [9, 29],
|
||||
},
|
||||
color: {
|
||||
black: [30, 39],
|
||||
red: [31, 39],
|
||||
green: [32, 39],
|
||||
yellow: [33, 39],
|
||||
blue: [34, 39],
|
||||
magenta: [35, 39],
|
||||
cyan: [36, 39],
|
||||
white: [37, 39],
|
||||
|
||||
// Bright color
|
||||
blackBright: [90, 39],
|
||||
gray: [90, 39], // Alias of `blackBright`
|
||||
grey: [90, 39], // Alias of `blackBright`
|
||||
redBright: [91, 39],
|
||||
greenBright: [92, 39],
|
||||
yellowBright: [93, 39],
|
||||
blueBright: [94, 39],
|
||||
magentaBright: [95, 39],
|
||||
cyanBright: [96, 39],
|
||||
whiteBright: [97, 39],
|
||||
},
|
||||
bgColor: {
|
||||
bgBlack: [40, 49],
|
||||
bgRed: [41, 49],
|
||||
bgGreen: [42, 49],
|
||||
bgYellow: [43, 49],
|
||||
bgBlue: [44, 49],
|
||||
bgMagenta: [45, 49],
|
||||
bgCyan: [46, 49],
|
||||
bgWhite: [47, 49],
|
||||
|
||||
// Bright color
|
||||
bgBlackBright: [100, 49],
|
||||
bgGray: [100, 49], // Alias of `bgBlackBright`
|
||||
bgGrey: [100, 49], // Alias of `bgBlackBright`
|
||||
bgRedBright: [101, 49],
|
||||
bgGreenBright: [102, 49],
|
||||
bgYellowBright: [103, 49],
|
||||
bgBlueBright: [104, 49],
|
||||
bgMagentaBright: [105, 49],
|
||||
bgCyanBright: [106, 49],
|
||||
bgWhiteBright: [107, 49],
|
||||
},
|
||||
};
|
||||
|
||||
export const modifierNames = Object.keys(styles.modifier);
|
||||
export const foregroundColorNames = Object.keys(styles.color);
|
||||
export const backgroundColorNames = Object.keys(styles.bgColor);
|
||||
export const colorNames = [...foregroundColorNames, ...backgroundColorNames];
|
||||
|
||||
function assembleStyles() {
|
||||
const codes = new Map();
|
||||
|
||||
for (const [groupName, group] of Object.entries(styles)) {
|
||||
for (const [styleName, style] of Object.entries(group)) {
|
||||
styles[styleName] = {
|
||||
open: `\u001B[${style[0]}m`,
|
||||
close: `\u001B[${style[1]}m`,
|
||||
};
|
||||
|
||||
group[styleName] = styles[styleName];
|
||||
|
||||
codes.set(style[0], style[1]);
|
||||
}
|
||||
|
||||
Object.defineProperty(styles, groupName, {
|
||||
value: group,
|
||||
enumerable: false,
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(styles, 'codes', {
|
||||
value: codes,
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
styles.color.close = '\u001B[39m';
|
||||
styles.bgColor.close = '\u001B[49m';
|
||||
|
||||
styles.color.ansi = wrapAnsi16();
|
||||
styles.color.ansi256 = wrapAnsi256();
|
||||
styles.color.ansi16m = wrapAnsi16m();
|
||||
styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
|
||||
styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
|
||||
styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
|
||||
|
||||
// From https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js
|
||||
Object.defineProperties(styles, {
|
||||
rgbToAnsi256: {
|
||||
value(red, green, blue) {
|
||||
// We use the extended greyscale palette here, with the exception of
|
||||
// black and white. normal palette only has 4 greyscale shades.
|
||||
if (red === green && green === blue) {
|
||||
if (red < 8) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
if (red > 248) {
|
||||
return 231;
|
||||
}
|
||||
|
||||
return Math.round(((red - 8) / 247) * 24) + 232;
|
||||
}
|
||||
|
||||
return 16
|
||||
+ (36 * Math.round(red / 255 * 5))
|
||||
+ (6 * Math.round(green / 255 * 5))
|
||||
+ Math.round(blue / 255 * 5);
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
hexToRgb: {
|
||||
value(hex) {
|
||||
const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
|
||||
if (!matches) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
let [colorString] = matches;
|
||||
|
||||
if (colorString.length === 3) {
|
||||
colorString = [...colorString].map(character => character + character).join('');
|
||||
}
|
||||
|
||||
const integer = Number.parseInt(colorString, 16);
|
||||
|
||||
return [
|
||||
/* eslint-disable no-bitwise */
|
||||
(integer >> 16) & 0xFF,
|
||||
(integer >> 8) & 0xFF,
|
||||
integer & 0xFF,
|
||||
/* eslint-enable no-bitwise */
|
||||
];
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
hexToAnsi256: {
|
||||
value: hex => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
|
||||
enumerable: false,
|
||||
},
|
||||
ansi256ToAnsi: {
|
||||
value(code) {
|
||||
if (code < 8) {
|
||||
return 30 + code;
|
||||
}
|
||||
|
||||
if (code < 16) {
|
||||
return 90 + (code - 8);
|
||||
}
|
||||
|
||||
let red;
|
||||
let green;
|
||||
let blue;
|
||||
|
||||
if (code >= 232) {
|
||||
red = (((code - 232) * 10) + 8) / 255;
|
||||
green = red;
|
||||
blue = red;
|
||||
} else {
|
||||
code -= 16;
|
||||
|
||||
const remainder = code % 36;
|
||||
|
||||
red = Math.floor(code / 36) / 5;
|
||||
green = Math.floor(remainder / 6) / 5;
|
||||
blue = (remainder % 6) / 5;
|
||||
}
|
||||
|
||||
const value = Math.max(red, green, blue) * 2;
|
||||
|
||||
if (value === 0) {
|
||||
return 30;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red));
|
||||
|
||||
if (value === 2) {
|
||||
result += 60;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
rgbToAnsi: {
|
||||
value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
|
||||
enumerable: false,
|
||||
},
|
||||
hexToAnsi: {
|
||||
value: hex => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
|
||||
enumerable: false,
|
||||
},
|
||||
});
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
const ansiStyles = assembleStyles();
|
||||
|
||||
export default ansiStyles;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {default} from './index.js';
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
const level = (() => {
|
||||
if (!('navigator' in globalThis)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (globalThis.navigator.userAgentData) {
|
||||
const brand = navigator.userAgentData.brands.find(({brand}) => brand === 'Chromium');
|
||||
if (brand && brand.version > 93) {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (/\b(Chrome|Chromium)\//.test(globalThis.navigator.userAgent)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})();
|
||||
|
||||
const colorSupport = level !== 0 && {
|
||||
level,
|
||||
hasBasic: true,
|
||||
has256: level >= 2,
|
||||
has16m: level >= 3,
|
||||
};
|
||||
|
||||
const supportsColor = {
|
||||
stdout: colorSupport,
|
||||
stderr: colorSupport,
|
||||
};
|
||||
|
||||
export default supportsColor;
|
||||
Generated
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
import type {WriteStream} from 'node:tty';
|
||||
|
||||
export type Options = {
|
||||
/**
|
||||
Whether `process.argv` should be sniffed for `--color` and `--no-color` flags.
|
||||
|
||||
@default true
|
||||
*/
|
||||
readonly sniffFlags?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
Levels:
|
||||
- `0` - All colors disabled.
|
||||
- `1` - Basic 16 colors support.
|
||||
- `2` - ANSI 256 colors support.
|
||||
- `3` - Truecolor 16 million colors support.
|
||||
*/
|
||||
export type ColorSupportLevel = 0 | 1 | 2 | 3;
|
||||
|
||||
/**
|
||||
Detect whether the terminal supports color.
|
||||
*/
|
||||
export type ColorSupport = {
|
||||
/**
|
||||
The color level.
|
||||
*/
|
||||
level: ColorSupportLevel;
|
||||
|
||||
/**
|
||||
Whether basic 16 colors are supported.
|
||||
*/
|
||||
hasBasic: boolean;
|
||||
|
||||
/**
|
||||
Whether ANSI 256 colors are supported.
|
||||
*/
|
||||
has256: boolean;
|
||||
|
||||
/**
|
||||
Whether Truecolor 16 million colors are supported.
|
||||
*/
|
||||
has16m: boolean;
|
||||
};
|
||||
|
||||
export type ColorInfo = ColorSupport | false;
|
||||
|
||||
export function createSupportsColor(stream?: WriteStream, options?: Options): ColorInfo;
|
||||
|
||||
declare const supportsColor: {
|
||||
stdout: ColorInfo;
|
||||
stderr: ColorInfo;
|
||||
};
|
||||
|
||||
export default supportsColor;
|
||||
Generated
Vendored
+190
@@ -0,0 +1,190 @@
|
||||
import process from 'node:process';
|
||||
import os from 'node:os';
|
||||
import tty from 'node:tty';
|
||||
|
||||
// From: https://github.com/sindresorhus/has-flag/blob/main/index.js
|
||||
/// function hasFlag(flag, argv = globalThis.Deno?.args ?? process.argv) {
|
||||
function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process.argv) {
|
||||
const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--');
|
||||
const position = argv.indexOf(prefix + flag);
|
||||
const terminatorPosition = argv.indexOf('--');
|
||||
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
|
||||
}
|
||||
|
||||
const {env} = process;
|
||||
|
||||
let flagForceColor;
|
||||
if (
|
||||
hasFlag('no-color')
|
||||
|| hasFlag('no-colors')
|
||||
|| hasFlag('color=false')
|
||||
|| hasFlag('color=never')
|
||||
) {
|
||||
flagForceColor = 0;
|
||||
} else if (
|
||||
hasFlag('color')
|
||||
|| hasFlag('colors')
|
||||
|| hasFlag('color=true')
|
||||
|| hasFlag('color=always')
|
||||
) {
|
||||
flagForceColor = 1;
|
||||
}
|
||||
|
||||
function envForceColor() {
|
||||
if ('FORCE_COLOR' in env) {
|
||||
if (env.FORCE_COLOR === 'true') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (env.FORCE_COLOR === 'false') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
|
||||
}
|
||||
}
|
||||
|
||||
function translateLevel(level) {
|
||||
if (level === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
level,
|
||||
hasBasic: true,
|
||||
has256: level >= 2,
|
||||
has16m: level >= 3,
|
||||
};
|
||||
}
|
||||
|
||||
function _supportsColor(haveStream, {streamIsTTY, sniffFlags = true} = {}) {
|
||||
const noFlagForceColor = envForceColor();
|
||||
if (noFlagForceColor !== undefined) {
|
||||
flagForceColor = noFlagForceColor;
|
||||
}
|
||||
|
||||
const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
|
||||
|
||||
if (forceColor === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sniffFlags) {
|
||||
if (hasFlag('color=16m')
|
||||
|| hasFlag('color=full')
|
||||
|| hasFlag('color=truecolor')) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (hasFlag('color=256')) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Azure DevOps pipelines.
|
||||
// Has to be above the `!streamIsTTY` check.
|
||||
if ('TF_BUILD' in env && 'AGENT_NAME' in env) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (haveStream && !streamIsTTY && forceColor === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const min = forceColor || 0;
|
||||
|
||||
if (env.TERM === 'dumb') {
|
||||
return min;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Windows 10 build 10586 is the first Windows release that supports 256 colors.
|
||||
// Windows 10 build 14931 is the first release that supports 16m/TrueColor.
|
||||
const osRelease = os.release().split('.');
|
||||
if (
|
||||
Number(osRelease[0]) >= 10
|
||||
&& Number(osRelease[2]) >= 10_586
|
||||
) {
|
||||
return Number(osRelease[2]) >= 14_931 ? 3 : 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ('CI' in env) {
|
||||
if (['GITHUB_ACTIONS', 'GITEA_ACTIONS', 'CIRCLECI'].some(key => key in env)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (['TRAVIS', 'APPVEYOR', 'GITLAB_CI', 'BUILDKITE', 'DRONE'].some(sign => sign in env) || env.CI_NAME === 'codeship') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
if ('TEAMCITY_VERSION' in env) {
|
||||
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
|
||||
}
|
||||
|
||||
if (env.COLORTERM === 'truecolor') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (env.TERM === 'xterm-kitty') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (env.TERM === 'xterm-ghostty') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (env.TERM === 'wezterm') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if ('TERM_PROGRAM' in env) {
|
||||
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10);
|
||||
|
||||
switch (env.TERM_PROGRAM) {
|
||||
case 'iTerm.app': {
|
||||
return version >= 3 ? 3 : 2;
|
||||
}
|
||||
|
||||
case 'Apple_Terminal': {
|
||||
return 2;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
}
|
||||
|
||||
if (/-256(color)?$/i.test(env.TERM)) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ('COLORTERM' in env) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
export function createSupportsColor(stream, options = {}) {
|
||||
const level = _supportsColor(stream, {
|
||||
streamIsTTY: stream && stream.isTTY,
|
||||
...options,
|
||||
});
|
||||
|
||||
return translateLevel(level);
|
||||
}
|
||||
|
||||
const supportsColor = {
|
||||
stdout: createSupportsColor({isTTY: tty.isatty(1)}),
|
||||
stderr: createSupportsColor({isTTY: tty.isatty(2)}),
|
||||
};
|
||||
|
||||
export default supportsColor;
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "chalk5-case", "private": true, "type": "module" }
|
||||
@@ -0,0 +1,97 @@
|
||||
/* chalk 5 compiled from its published JavaScript (tests/fixtures/npm/cases/
|
||||
* chalk5): the program runs under Node and as a native binary with each
|
||||
* color-detection input supports-color reads — FORCE_COLOR values and the
|
||||
* --color/--no-color flags — and stdout, stderr and the exit status must
|
||||
* match byte for byte. npm.test.ts runs the same case once in the
|
||||
* harness's own environment. */
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { beforeAll, describe, expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../..");
|
||||
const entry = join(repoRoot, "tests/fixtures/npm/cases/chalk5/main.ts");
|
||||
const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/chalk5");
|
||||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||||
|
||||
interface RunResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
}
|
||||
|
||||
async function run(cmd: string, args: string[], env: NodeJS.ProcessEnv): Promise<RunResult> {
|
||||
try {
|
||||
const pending = execFileAsync(cmd, args, { cwd: dirname(entry), env, encoding: "utf8" });
|
||||
pending.child.stdin?.end();
|
||||
const { stdout, stderr } = await pending;
|
||||
return { stdout, stderr, exitCode: 0 };
|
||||
} catch (err) {
|
||||
const e = err as { code?: unknown; stdout?: string; stderr?: string };
|
||||
if (typeof e.stdout !== "string" || typeof e.stderr !== "string") throw err;
|
||||
return {
|
||||
stdout: e.stdout,
|
||||
stderr: e.stderr,
|
||||
exitCode: typeof e.code === "number" ? e.code : -1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** The harness environment without the color inputs, plus `force`. */
|
||||
function colorEnv(force: string | undefined): NodeJS.ProcessEnv {
|
||||
const env = { ...process.env };
|
||||
delete env["FORCE_COLOR"];
|
||||
delete env["NO_COLOR"];
|
||||
delete env["NODE_DISABLE_COLORS"];
|
||||
if (force !== undefined) env["FORCE_COLOR"] = force;
|
||||
return env;
|
||||
}
|
||||
|
||||
const cases: { name: string; force: string | undefined; argv: string[] }[] = [
|
||||
{ name: "FORCE_COLOR unset", force: undefined, argv: [] },
|
||||
...["0", "1", "2", "3", "true", "false", ""].map((force) => ({
|
||||
name: `FORCE_COLOR=${JSON.stringify(force)}`,
|
||||
force,
|
||||
argv: [],
|
||||
})),
|
||||
...[
|
||||
["--no-color"],
|
||||
["--color"],
|
||||
["--color=256"],
|
||||
["--color=16m"],
|
||||
["--color", "--", "--no-color"],
|
||||
].map((argv) => ({ name: argv.join(" "), force: undefined, argv })),
|
||||
{ name: "FORCE_COLOR=3 --no-color", force: "3", argv: ["--no-color"] },
|
||||
];
|
||||
|
||||
let binary = "";
|
||||
|
||||
beforeAll(async () => {
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const result = await compile(entry, { outPath: join(outDir, "program"), outDir, sanitize });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
binary = result.binaryPath;
|
||||
}, 300_000);
|
||||
|
||||
describe("chalk 5 color detection matches Node", () => {
|
||||
for (const c of cases) {
|
||||
test(
|
||||
c.name,
|
||||
async () => {
|
||||
const env = colorEnv(c.force);
|
||||
const [node, native] = await Promise.all([
|
||||
run(process.execPath, [entry, ...c.argv], env),
|
||||
run(binary, c.argv, env),
|
||||
]);
|
||||
expect(native.stdout).toBe(node.stdout);
|
||||
expect(native.stderr).toBe(node.stderr);
|
||||
expect(native.exitCode).toBe(node.exitCode);
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
}
|
||||
});
|
||||
+5437
-5419
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user