From 1090c05dfe6be947af2e821f74e17ab16d872da9 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 08:57:56 -0700 Subject: [PATCH] Compile chalk 5 through the dynamic object model chalk's callable builder (a function whose prototype chain is set with Object.setPrototypeOf, with style getters from Object.defineProperties) already ran on the checked-dynamic model. What failed was the code around it. Each fix is general and sits on a path that used to end in a refusal or a runtime fence, or is confined to JavaScript files: - package.json "imports" inside a --npm-static package resolve to the shipped JS with the "node" condition hoisted, like the tsgo fs shadow; - JavaScript object literals that the file reshapes at run time (keyed writes with runtime keys, delete, Object.defineProperty/ies) map to the checked-dynamic type, with the literals nested in them; - a JavaScript class whose only member is a constructor that returns a value and never reads `this` answers that value from `new` (the return used to be discarded silently); - `const { env } = process` binds the data member in JavaScript; - `o && x` / `o || x` with an always-truthy object, array or function left operand; - Number.parseInt/parseFloat over checked-dynamic values apply ToString; - Function.prototype as a value is one compiler-emitted function; - calls of String.prototype-only names (bold, italics, ...) on untyped receivers call the receiver's own or inherited member; a string receiver throws the refusal at run time. Programs without these patterns compile to identical bytes: the runtime benchmark workloads, the self-hosted compiler (LLVM and executable) and the TypeScript corpus IR were compared against the baseline. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/builtins/numbers.ts | 18 +- .../src/frontend/lowering/builtins/process.ts | 35 ++ .../lowering/js-dynamic-objects.test.ts | 38 ++ .../frontend/lowering/js-dynamic-objects.ts | 312 +++++++++++++++++ .../src/frontend/lowering/lower-calls.ts | 57 ++- .../src/frontend/lowering/lower-classes.ts | 3 + .../src/frontend/lowering/lower-exprs.ts | 56 +++ .../src/frontend/lowering/lower-stmts.ts | 33 +- .../compiler/src/frontend/lowering/lowerer.ts | 3 + packages/compiler/src/frontend/npm-static.ts | 4 + .../compiler/src/frontend/resolve.test.ts | 54 +++ packages/compiler/src/frontend/resolve.ts | 12 + packages/compiler/src/frontend/type-mapper.ts | 14 + .../main.ts.json | 7 + .../tests/corpus/js-callable-builder.js.json | 6 + .../js-class-constructor-returns/main.ts.json | 7 + .../js-function-prototype-value.js.json | 6 + .../corpus/js-number-parse-dynamic.js.json | 6 + .../corpus/js-process-destructure.mjs.json | 6 + .../corpus/js-reshaped-object-literal.js.json | 6 + .../corpus/truthy-object-logical.ts.json | 6 + .../fixtures/npm/cases/chalk5/main.ts.json | 6 + .../js-builder-string-method-names/builder.js | 22 ++ .../js-builder-string-method-names/main.ts | 14 + tests/corpus/js-callable-builder.js | 110 ++++++ .../js-class-constructor-returns/factory.js | 35 ++ .../js-class-constructor-returns/main.ts | 11 + tests/corpus/js-function-prototype-value.js | 17 + tests/corpus/js-number-parse-dynamic.js | 8 + tests/corpus/js-process-destructure.mjs | 12 + tests/corpus/js-reshaped-object-literal.js | 42 +++ tests/corpus/truthy-object-logical.ts | 31 ++ tests/fixtures/npm/cases/chalk5/main.ts | 64 ++++ .../cases/chalk5/node_modules/chalk/license | 9 + .../chalk5/node_modules/chalk/package.json | 83 +++++ .../node_modules/chalk/source/index.d.ts | 325 ++++++++++++++++++ .../chalk5/node_modules/chalk/source/index.js | 225 ++++++++++++ .../node_modules/chalk/source/utilities.js | 33 ++ .../source/vendor/ansi-styles/index.d.ts | 236 +++++++++++++ .../chalk/source/vendor/ansi-styles/index.js | 223 ++++++++++++ .../source/vendor/supports-color/browser.d.ts | 1 + .../source/vendor/supports-color/browser.js | 34 ++ .../source/vendor/supports-color/index.d.ts | 55 +++ .../source/vendor/supports-color/index.js | 190 ++++++++++ tests/fixtures/npm/cases/chalk5/package.json | 1 + tests/harness/chalk.test.ts | 97 ++++++ 46 files changed, 2569 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/js-dynamic-objects.test.ts create mode 100644 packages/compiler/src/frontend/lowering/js-dynamic-objects.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-builder-string-method-names/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-callable-builder.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-class-constructor-returns/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-prototype-value.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-number-parse-dynamic.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-process-destructure.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-reshaped-object-literal.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/truthy-object-logical.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json create mode 100644 tests/corpus/js-builder-string-method-names/builder.js create mode 100644 tests/corpus/js-builder-string-method-names/main.ts create mode 100644 tests/corpus/js-callable-builder.js create mode 100644 tests/corpus/js-class-constructor-returns/factory.js create mode 100644 tests/corpus/js-class-constructor-returns/main.ts create mode 100644 tests/corpus/js-function-prototype-value.js create mode 100644 tests/corpus/js-number-parse-dynamic.js create mode 100644 tests/corpus/js-process-destructure.mjs create mode 100644 tests/corpus/js-reshaped-object-literal.js create mode 100644 tests/corpus/truthy-object-logical.ts create mode 100644 tests/fixtures/npm/cases/chalk5/main.ts create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/license create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/package.json create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/index.d.ts create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/index.js create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/utilities.js create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/ansi-styles/index.d.ts create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/ansi-styles/index.js create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/browser.d.ts create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/browser.js create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/index.d.ts create mode 100644 tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/index.js create mode 100644 tests/fixtures/npm/cases/chalk5/package.json create mode 100644 tests/harness/chalk.test.ts diff --git a/packages/compiler/src/frontend/lowering/builtins/numbers.ts b/packages/compiler/src/frontend/lowering/builtins/numbers.ts index 08d6cb3494..4f86e6f74e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/numbers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/numbers.ts @@ -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) diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 97bf1e9637..94c2cdba72 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -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, diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.test.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.test.ts new file mode 100644 index 0000000000..23ebdfaa91 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.test.ts @@ -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); +}); diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts new file mode 100644 index 0000000000..1d7ab6eeed --- /dev/null +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -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 = 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): 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 { + const out = new Set(); + for (const file of files) { + if (!isJsSourceFile(file) || file.isDeclarationFile) continue; + const literals = new Map(); + const reshaped = new Set(); + 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, + }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 56c639397b..04034c2e0f 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -3463,13 +3463,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) }, @@ -3481,7 +3509,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); @@ -3590,6 +3618,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 diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 06739166c1..b5d0e4ac87 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -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"; @@ -8422,6 +8423,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); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d2d990e166..977062d45f 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -4974,6 +4974,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") @@ -11203,6 +11231,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 { diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index cffe4c6619..9e3375e6d8 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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 { bindingSource } from "../binding-source.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -3131,6 +3132,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; @@ -3166,8 +3170,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 @@ -3187,6 +3192,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.` read the destructuring performs. } else { lowerer.unsupported( "SC1031", @@ -3203,6 +3215,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", @@ -3215,6 +3231,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); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 087b4e628b..4193be2bb6 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,3 +1,4 @@ +import { jsDynamicDeclarations } from "./js-dynamic-objects.js"; import { lifetimeRegionType, lowerLifetimeRegionCall, @@ -2860,6 +2861,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); diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index 1f83a12d40..d2f4ea3e8b 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -190,6 +190,10 @@ export function npmStaticTransformPkgJson(pkg: Record): 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 diff --git a/packages/compiler/src/frontend/resolve.test.ts b/packages/compiler/src/frontend/resolve.test.ts index 0d135b3c29..0dd09e73d3 100644 --- a/packages/compiler/src/frontend/resolve.test.ts +++ b/packages/compiler/src/frontend/resolve.test.ts @@ -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 }); + } +}); diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index db440c1996..4634004755 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -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; + 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 diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 52964e1bde..1e1bedc39e 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -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 }; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-builder-string-method-names/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-builder-string-method-names/main.ts.json new file mode 100644 index 0000000000..d1f487f89b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-builder-string-method-names/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/js-builder-string-method-names/builder.js", + "/tests/corpus/js-builder-string-method-names/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-callable-builder.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-callable-builder.js.json new file mode 100644 index 0000000000..8f1025c47c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-callable-builder.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/js-callable-builder.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-class-constructor-returns/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-class-constructor-returns/main.ts.json new file mode 100644 index 0000000000..e05f1760c9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-class-constructor-returns/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/js-class-constructor-returns/factory.js", + "/tests/corpus/js-class-constructor-returns/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-prototype-value.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-prototype-value.js.json new file mode 100644 index 0000000000..b0fa827e1a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-prototype-value.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/js-function-prototype-value.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-number-parse-dynamic.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-number-parse-dynamic.js.json new file mode 100644 index 0000000000..608e24d404 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-number-parse-dynamic.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/js-number-parse-dynamic.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-process-destructure.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-process-destructure.mjs.json new file mode 100644 index 0000000000..87d43324eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-process-destructure.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/js-process-destructure.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-reshaped-object-literal.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-reshaped-object-literal.js.json new file mode 100644 index 0000000000..939240b2ce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-reshaped-object-literal.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/js-reshaped-object-literal.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/truthy-object-logical.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/truthy-object-logical.ts.json new file mode 100644 index 0000000000..96661c6301 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/truthy-object-logical.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/truthy-object-logical.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json new file mode 100644 index 0000000000..065c963470 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/chalk5/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/chalk5/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/js-builder-string-method-names/builder.js b/tests/corpus/js-builder-string-method-names/builder.js new file mode 100644 index 0000000000..36824c3179 --- /dev/null +++ b/tests/corpus/js-builder-string-method-names/builder.js @@ -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 + ``); + 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; diff --git a/tests/corpus/js-builder-string-method-names/main.ts b/tests/corpus/js-builder-string-method-names/main.ts new file mode 100644 index 0000000000..5cab8e4ca7 --- /dev/null +++ b/tests/corpus/js-builder-string-method-names/main.ts @@ -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")); diff --git a/tests/corpus/js-callable-builder.js b/tests/corpus/js-callable-builder.js new file mode 100644 index 0000000000..3697d788dd --- /dev/null +++ b/tests/corpus/js-callable-builder.js @@ -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}>`, ``, 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(" 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); diff --git a/tests/corpus/js-class-constructor-returns/factory.js b/tests/corpus/js-class-constructor-returns/factory.js new file mode 100644 index 0000000000..9fe0b84c4d --- /dev/null +++ b/tests/corpus/js-class-constructor-returns/factory.js @@ -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); diff --git a/tests/corpus/js-class-constructor-returns/main.ts b/tests/corpus/js-class-constructor-returns/main.ts new file mode 100644 index 0000000000..8000163292 --- /dev/null +++ b/tests/corpus/js-class-constructor-returns/main.ts @@ -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); diff --git a/tests/corpus/js-function-prototype-value.js b/tests/corpus/js-function-prototype-value.js new file mode 100644 index 0000000000..3efe4540ee --- /dev/null +++ b/tests/corpus/js-function-prototype-value.js @@ -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); diff --git a/tests/corpus/js-number-parse-dynamic.js b/tests/corpus/js-number-parse-dynamic.js new file mode 100644 index 0000000000..e4b0500463 --- /dev/null +++ b/tests/corpus/js-number-parse-dynamic.js @@ -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)); diff --git a/tests/corpus/js-process-destructure.mjs b/tests/corpus/js-process-destructure.mjs new file mode 100644 index 0000000000..81660e22c5 --- /dev/null +++ b/tests/corpus/js-process-destructure.mjs @@ -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(" ")); diff --git a/tests/corpus/js-reshaped-object-literal.js b/tests/corpus/js-reshaped-object-literal.js new file mode 100644 index 0000000000..c184cc388a --- /dev/null +++ b/tests/corpus/js-reshaped-object-literal.js @@ -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: `` }; + 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 = ""; + 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); diff --git a/tests/corpus/truthy-object-logical.ts b/tests/corpus/truthy-object-logical.ts new file mode 100644 index 0000000000..0b84b760bb --- /dev/null +++ b/tests/corpus/truthy-object-logical.ts @@ -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); diff --git a/tests/fixtures/npm/cases/chalk5/main.ts b/tests/fixtures/npm/cases/chalk5/main.ts new file mode 100644 index 0000000000..6a33a1466d --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/main.ts @@ -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)); diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/license b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/license new file mode 100644 index 0000000000..fa7ceba3eb --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/license @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (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. diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/package.json b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/package.json new file mode 100644 index 0000000000..c9e0dc52ba --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/package.json @@ -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" + ] + } +} diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/index.d.ts b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/index.d.ts new file mode 100644 index 0000000000..8295d92523 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/index.d.ts @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/index.js b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/index.js new file mode 100644 index 0000000000..8bc993da5d --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/index.js @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/utilities.js b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/utilities.js new file mode 100644 index 0000000000..4366dee0d8 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/utilities.js @@ -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; +} diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/ansi-styles/index.d.ts b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/ansi-styles/index.d.ts new file mode 100644 index 0000000000..58f133abe9 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/ansi-styles/index.d.ts @@ -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; +} & ForegroundColor & BackgroundColor & Modifier & ConvertColor; + +export default ansiStyles; diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/ansi-styles/index.js b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/ansi-styles/index.js new file mode 100644 index 0000000000..eaa7bed6cb --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/ansi-styles/index.js @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/browser.d.ts b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/browser.d.ts new file mode 100644 index 0000000000..0e8a9ccb86 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/browser.d.ts @@ -0,0 +1 @@ +export {default} from './index.js'; diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/browser.js b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/browser.js new file mode 100644 index 0000000000..fbb6ce0fc9 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/browser.js @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/index.d.ts b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/index.d.ts new file mode 100644 index 0000000000..db44a78519 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/index.d.ts @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/index.js b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/index.js new file mode 100644 index 0000000000..265d7f8581 --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/node_modules/chalk/source/vendor/supports-color/index.js @@ -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; diff --git a/tests/fixtures/npm/cases/chalk5/package.json b/tests/fixtures/npm/cases/chalk5/package.json new file mode 100644 index 0000000000..3804daa8df --- /dev/null +++ b/tests/fixtures/npm/cases/chalk5/package.json @@ -0,0 +1 @@ +{ "name": "chalk5-case", "private": true, "type": "module" } diff --git a/tests/harness/chalk.test.ts b/tests/harness/chalk.test.ts new file mode 100644 index 0000000000..4d99bf3ee6 --- /dev/null +++ b/tests/harness/chalk.test.ts @@ -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 { + 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, + ); + } +});