Merge remote-tracking branch 'origin/ni/i4-deferred' into HEAD

This commit is contained in:
Malte Ubl
2026-10-23 08:09:07 -07:00
117 changed files with 2006 additions and 200 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ Type-only imports follow Node.js's type stripping. `import type` and `export typ
scriptc compiles a package automatically when it:
- ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and
- ships readable JavaScript: unminified, or minified by a bundler whose helpers keep their names.
- ships JavaScript that isn't a webpack bundle. Minified JavaScript compiles like unminified JavaScript.
The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use.
@@ -18,7 +18,7 @@
Stages may take the `{ signal }` argument.
With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`.
`@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`).
In typed code, the callback of a pipeline with an async function destination receives `null` instead of `undefined` when there is no error.
In typed code, a pipeline callback with an async function or async generator destination receives `null` for no error when its error parameter cannot hold `undefined` (@types/node declares it `NodeJS.ErrnoException | null`), and `undefined` when it can, as in Node.
`Readable.from` accepts `objectMode` and `highWaterMark` options.
The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options.
Their callbacks may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream.
@@ -32,9 +32,10 @@
`readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read.
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on Writable and Duplex; `writev` on a Transform or PassThrough, assigning `_construct` after construction, and a non-literal `decodeStrings` or `defaultEncoding` in an options object literal are unsupported.
The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on every class with a writable side.
A `decodeStrings` or `defaultEncoding` value known only at run time is unsupported after an option member whose value has effects (a call or an assignment).
In typed code, a `writev` entry's `encoding` declared as a string receives `""` where Node passes `undefined` or `null` (an object-mode write).
`setDefaultEncoding()` takes any encoding Node knows.
A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error.
`node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
`on`, `once`, and `addListener` return the interface, so registrations chain.
The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node.
@@ -42,7 +43,7 @@
The output's `'resize'` event does not redraw the line.
The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end.
In JavaScript, a `'history'` listener receives a copy of the history; its changes are written back when the listener returns, but the copy is not `rl.history` itself.
`rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node, except that `rl.cursor` of an interface that is not a terminal throws instead of answering `undefined`.
`rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node; `rl.cursor` is `undefined` on an interface that is not a terminal.
`rl[Symbol.dispose]()` closes the interface, and `instanceof` with `readline.Interface` or `readlinePromises.Interface` answers whether the interface came from that module, as in Node.
In JavaScript, the input and output can be any expression that evaluates to a stream, such as a function's result or an object's member; a value that is not a stream is unsupported.
In JavaScript, `question()` and `setPrompt()` accept a value that is not a string: when such a prompt is written, an interface that is not a terminal throws `ERR_INVALID_ARG_TYPE` as Node does (the `readline/promises` question rejects with it) and the question stays pending, and a terminal interface writes `String(value)`.
+2 -2
View File
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" },
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
runtime_pack: {
kind: "precompiled",
package: "@scriptc/runtime-darwin-arm64",
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
() => null,
(failure: { stderr?: string }) => failure,
);
expect(error?.stderr).toContain("scr_runtime_abi_v36");
expect(error?.stderr).toContain("scr_runtime_abi_v37");
});
});
@@ -727,6 +727,17 @@ export function streamCbThunkFor(
const ty = host.llType(p);
host.declare(`declare zeroext i1 @scr_exc_pending()`);
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
// An entry `encoding` declared as a string (the declarations'
// `encoding: string`) receives "" where Node passes undefined or
// null (an object-mode write), as a write's encoding parameter does.
const entry =
p.kind === "array" && p.elem.kind === "record"
? host.recordsById.get(p.elem.shapeId)
: undefined;
if (entry?.fields.some((f) => f.name === "encoding" && f.type.kind === "string")) {
host.declare(`declare void @scr_stream_writev_fill_encodings(ptr)`);
d.push(` call void @scr_stream_writev_fill_encodings(ptr %chunks)`);
}
d.push(
` %vt${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %chunks, ptr null)`,
` %vtbad${i} = call zeroext i1 @scr_exc_pending()`,
@@ -1159,6 +1159,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"readable.pushEncoding": "scr_stream_set_push_encoding",
"stream.objectMode": "scr_stream_object_mode",
"stream.decodeStrings": "scr_stream_decode_strings",
"stream.optionValues": "scr_stream_option_values",
"stream.checkEncodingOption": "scr_stream_check_encoding_option",
"stream.duplexFromGen": "scr_stream_duplex_from_gen",
"stream.duplexFromDyn": "scr_stream_duplex_from_dyn",
"stream.duplexifyReadable": "scr_stream_duplexify_readable",
@@ -1793,6 +1795,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"stream.setTransform",
"stream.setFlush",
"stream.setConstruct",
"stream.replaceConstruct",
"stream.duplexFromGen",
"stream.duplexFromDyn",
"stream.duplexifyReadable",
@@ -189,6 +189,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
e.fn === "stream.setTransform" ||
e.fn === "stream.setFlush" ||
e.fn === "stream.setConstruct" ||
e.fn === "stream.replaceConstruct" ||
e.fn === "stream.setWritev" ||
e.fn === "stream.setWritevDefault"
) {
@@ -203,6 +204,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
"stream.setTransform": "t",
"stream.setFlush": "l",
"stream.setConstruct": "c",
"stream.replaceConstruct": "c",
"stream.setWritev": "v",
"stream.setWritevDefault": "v",
};
@@ -214,6 +216,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
"stream.setTransform": "scr_stream_set_transform",
"stream.setFlush": "scr_stream_set_flush",
"stream.setConstruct": "scr_stream_set_construct",
"stream.replaceConstruct": "scr_stream_replace_construct",
"stream.setWritev": "scr_stream_set_writev",
"stream.setWritevDefault": "scr_stream_set_writev_default",
};
+2 -2
View File
@@ -2,8 +2,8 @@
* of the link contract: a program object keeps this symbol undefined and the
* matching runtime defines it, so incompatible manual links fail before the
* program can start. */
export const RUNTIME_ABI_VERSION = 36 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v36" as const;
export const RUNTIME_ABI_VERSION = 37 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v37" as const;
/** Object consumption is useful today, but the complete scr_* surface may
* still change before 1.0. The versioned marker prevents accidental mixing;
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" },
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
flavors: {
@@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" },
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
compiler: {
command: "clang",
identity: "fixture clang",
+3 -3
View File
@@ -84,9 +84,9 @@ Options:
--npm-static <pkg[,pkg…]|auto>
compile the named npm packages' shipped JS statically as
program modules (repeatable; "auto" opts in every eligible
direct import: own .d.ts, unminified JS, no build-transform
markers; "pkg..." also names every package pkg depends
on). Packages that fail preflight are refused with a
direct import: own .d.ts, no build-transform markers;
"pkg..." also names every package pkg depends on).
Packages that fail preflight are refused with a
diagnostic. Eligible dependencies compile automatically.
--provenance-sources
EXPERIMENTAL: compile npm dependencies from their
@@ -6,9 +6,11 @@ import {
DYN,
F64,
type IrExpr,
type IrType,
type IrStmt,
type SrcLoc,
STRING,
UNDEFINED_T,
VOID,
arrayOf,
BYTES_U8,
@@ -54,6 +56,21 @@ export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean
});
}
/** `rl.cursor`: @types/node declares a number, but an interface that is
* not a terminal answers undefined, so the read keeps its
* `number | undefined` union. */
export function isOptionalReadlineCursorRead(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
): boolean {
return (
expr.name.text === "cursor" &&
!expr.questionDotToken &&
isReadlineTyped(lowerer, expr.expression) &&
lowerer.isStdlibMember(expr)
);
}
/** The runtime's listener event numbers (scr_readline.c). */
const RL_EVENTS: Record<string, number> = {
line: 0,
@@ -1341,7 +1358,7 @@ export function lowerReadlineCreateCall(
/** Property reads on an Interface: `history` (the history option itself,
* or the interface's own empty array), `historySize`, `historyIndex`,
* `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `terminal` and
* `cursor` (a non-terminal interface's undefined cursor throws SC2020).
* `cursor` (undefined on a non-terminal interface).
* Null for other receivers. */
export function lowerReadlineProperty(
lowerer: Lowerer,
@@ -1373,10 +1390,38 @@ export function lowerReadlineProperty(
loc,
};
}
const reads: Record<string, ["rl.line" | "rl.isTerminal" | "rl.cursor", IrExpr["type"]]> = {
if (name === "cursor") {
// `number | undefined`: a non-terminal interface has no cursor
const recv = lowerer.lowerExprExpecting(access.expression, F64);
const type: IrType = { kind: "union", unionId: lowerer.unions.intern([F64, UNDEFINED_T]) };
const local = recv.kind === "varRef" ? null : lowerer.declareHiddenLocal("%rlCursor", F64);
const id: IrExpr = local ? { kind: "varRef", localId: local.id, type: F64, loc } : recv;
const result: IrExpr = {
kind: "ternary",
cond: { kind: "libCall", fn: "rl.isTerminal", args: [id], type: BOOL, loc },
then: lowerer.coerceToExpected(
{ kind: "libCall", fn: "rl.cursor", args: [id], type: F64, loc },
type,
),
else_: lowerer.coerceToExpected(
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
type,
),
type,
loc,
};
if (!local) return result;
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: recv, loc }],
result,
type,
loc,
};
}
const reads: Record<string, ["rl.line" | "rl.isTerminal", IrExpr["type"]]> = {
line: ["rl.line", STRING],
terminal: ["rl.isTerminal", BOOL],
cursor: ["rl.cursor", F64],
};
const read = reads[name];
if (read !== undefined) {
@@ -2,6 +2,7 @@ import { keyedHelperName } from "../stable-names.js";
import * as ts from "../../ts7/adapter.js";
import {
BOOL,
DYN,
F64,
REGEX,
STRING,
@@ -14,7 +15,7 @@ import {
type IrType,
type SrcLoc,
} from "../../../ir/ir.js";
import { numLit, strLit, varRef } from "../../../ir/build.js";
import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js";
import { InternalCompilerError } from "../../../errors.js";
import { locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
@@ -65,14 +66,14 @@ export function lowerStringReplacement(
const search = argument(args[0], false);
const replacement = argument(args[1], true);
if (search.type.kind === "dyn" || replacement.type.kind === "dyn") {
if (replacement.type.kind === "func") {
lowerer.noLowering(`string .${method} with checked search values and callbacks`, call);
}
// Evaluate all inputs once before choosing the search protocol. Regexes
// retain their native pattern; plain values use the string-search helper.
// A typed callback receives the regex match's arguments or the plain
// search's (match, offset, subject).
const values = [receiver, search, replacement];
const locals = values.map((value) => lowerer.declareHiddenLocal("%replaceArg", value.type));
const refs = locals.map((local) => varRef(local.id, local.type, loc));
const callback = replacement.type.kind === "func";
const checkedReplacement =
replacement.type.kind === "dyn"
? lowerer.coerceInto(args[1] ?? call, refs[2]!, STRING)
@@ -91,14 +92,27 @@ export function lowerStringReplacement(
? {
kind: "ternary",
cond: { kind: "libCall", fn: "dyn.nativeRegexIs", args: [refs[1]!], type: BOOL, loc },
then: {
kind: "regexIntrinsic",
method,
receiver: refs[0]!,
args: [lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), checkedReplacement],
type: STRING,
loc,
},
then: callback
? {
kind: "libCall",
fn: "regex.replaceCallback",
args: [
refs[0]!,
lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX),
lowerer.coerceToExpected(refs[2]!, DYN),
{ kind: "boolLit", value: method === "replaceAll", type: BOOL, loc },
],
type: STRING,
loc,
}
: {
kind: "regexIntrinsic",
method,
receiver: refs[0]!,
args: [lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), checkedReplacement],
type: STRING,
loc,
},
else_: plain,
type: STRING,
loc,
@@ -148,11 +162,17 @@ function lowerStringReplacementValues(
if (callback.kind !== "func") {
throw new InternalCompilerError(`string .${method} replacement of type ${callback.kind}`);
}
// A checked parameter receives the boxed argument (undefined after the
// subject), and a checked, number or boolean result converts with
// ToString.
const full = [STRING, F64, STRING];
const ret = callback.ret.kind;
if (
callback.ret.kind !== "string" ||
callback.params.length > full.length ||
!callback.params.every((type, index) => typeEquals(type, full[index]!))
(ret !== "string" && ret !== "dyn" && ret !== "f64" && ret !== "bool") ||
!callback.params.every(
(type, index) =>
type.kind === "dyn" || (index < full.length && typeEquals(type, full[index]!)),
)
) {
lowerer.noLowering(
`string .${method} callback signature`,
@@ -175,13 +195,21 @@ function lowerStringReplacementValues(
const chunksType = arrayOf(STRING);
const chunks = varRef("chunks.0", chunksType, loc);
const builder = stringBuilder(loc, chunks);
const value: IrExpr = {
const result: IrExpr = {
kind: "callValue",
callee: replace,
args: [needle, match, subject].slice(0, callback.params.length),
type: STRING,
args: callback.params.map((type, index) => {
const arg = [needle, match, subject][index];
if (arg === undefined) return dynUndefinedExpr(loc);
return type.kind === "dyn" ? lowerer.coerceToExpected(arg, DYN) : arg;
}),
type: callback.ret,
loc,
};
const value: IrExpr =
ret === "dyn"
? { kind: "libCall", fn: "dyn.toStringCoerce", args: [result], type: STRING, loc }
: lowerer.ensureString(result, call);
const stop: IrStmt = { kind: "break", loc };
const body: IrStmt[] = [
builder.declare("chunks.0", { kind: "arrayLit", elems: [], type: chunksType, loc }),
@@ -2841,6 +2841,24 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
loc,
};
}
// A JavaScript default that tsc types `any` (`e.title || null` in an
// instance of a function with untyped parameters, where `e.title` is a
// string): the checked domain, as for union operands above.
if (
target === null &&
isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
lowerer.dynConvertible(left.type) &&
lowerer.dynConvertible(right.type)
) {
return {
kind: "logical",
op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||",
left: lowerer.coerceInto(expr.left, left, DYN),
right: lowerer.coerceInto(expr.right, right, DYN),
type: DYN,
loc,
};
}
lowerer.unsupported("SC1042", expr);
}
return {
@@ -33,6 +33,7 @@ import {
isOptionalProcessVersionsRead,
} from "../builtins/process.js";
import { requestGeneralStreamSurface } from "../general-streams.js";
import { isOptionalReadlineCursorRead } from "../builtins/readline.js";
import { literalValues } from "../../literal-values.js";
import { literalUnionArm } from "../../union-discriminants.js";
import { checkedArmExtraction } from "../coercions/unions.js";
@@ -617,6 +618,8 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node))
return expr;
if (isOptionalProcessVersionsRead(lowerer, node)) return expr;
if (ts.isPropertyAccessExpression(node) && isOptionalReadlineCursorRead(lowerer, node))
return expr;
// Represented fields and index values can include undefined even when
// checker inference or lib.d.ts declares strings (RegExp captures).
@@ -308,6 +308,22 @@ function mayHoldError(lowerer: Lowerer, type: ts.Type, errorType: ts.Type | null
);
}
/** The `f` of a `(0, f)` callee (a numeric left operand, so dropping it
* changes only the call's receiver), or null. */
function receiverlessCalleeOf(callee: ts.Expression): ts.Expression | null {
let e = callee;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (
!ts.isBinaryExpression(e) ||
e.operatorToken.kind !== ts.SyntaxKind.CommaToken ||
!ts.isNumericLiteral(e.left)
)
return null;
let right = e.right;
while (ts.isParenthesizedExpression(right)) right = right.expression;
return right;
}
export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
if (lowerer.errorAccessorReflection === null) noteErrorAccessorReflection(lowerer, expr);
@@ -331,6 +347,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const json = lowerJsonRequireCall(lowerer, expr);
if (json) return json;
// `(0, ns.member)(...)` over a builtin module (tsc's and esbuild's call
// without a receiver): Node's module functions do not read `this`, so it
// is the member call.
{
const member = receiverlessCalleeOf(expr.expression);
if (member !== null && ts.isPropertyAccessExpression(member)) {
const builtin = lowerer.lowerNamespaceBuiltinCall(expr, member);
if (builtin) return builtin;
}
}
// eval(...) and Function(...): Node's EvalError under
// --disallow-code-generation-from-strings (code-generation.ts).
{
@@ -2744,8 +2744,8 @@ export function collectClassShapeInner(
// state, so an override or shadowing field would split-brain.
// Underscore methods are the SUPPORTED override form — but only
// the ones the class's own base consumes (a `_read` on a
// Transform, or `_writev`/`_construct` anywhere, would be consumed
// by Node machinery that has no lowering here).
// Transform, or `_writev` on a Readable, would be consumed by Node
// machinery that has no lowering here).
if (memberName && ts.isIdentifier(memberName)) {
const streamBase = (() => {
for (let c = base; c; c = c.base) if (c.builtinStream) return c;
@@ -2764,7 +2764,7 @@ export function collectClassShapeInner(
lowerer.unsupported(
"SC1090",
memberName,
`declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (batched writes are supported on Writable and Duplex subclasses)`,
`declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (a Readable has no writable side)`,
);
}
const accepted = streamCtorShape(streamBase.def.name).accepted;
@@ -106,6 +106,61 @@ function pureRead(node: ts.Expression): boolean {
return false;
}
/** A literal, `undefined`, or a function expression: a value that reads
* no binding or property. */
function constantOptionValue(node: ts.Expression): boolean {
let e = node;
while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertion(e))
e = e.expression;
if (ts.isIdentifier(e)) return e.text === "undefined";
if (
ts.isPrefixUnaryExpression(e) &&
(e.operator === ts.SyntaxKind.MinusToken ||
e.operator === ts.SyntaxKind.PlusToken ||
e.operator === ts.SyntaxKind.ExclamationToken)
)
return constantOptionValue(e.operand);
return (
ts.isStringLiteralLike(e) ||
ts.isNumericLiteral(e) ||
ts.isBigIntLiteral(e) ||
e.kind === ts.SyntaxKind.TrueKeyword ||
e.kind === ts.SyntaxKind.FalseKeyword ||
e.kind === ts.SyntaxKind.NullKeyword ||
ts.isArrowFunction(e) ||
ts.isFunctionExpression(e)
);
}
/** Whether a runtime decodeStrings or defaultEncoding value (`prop`'s
* `value`) may be read before the other option members. Those values are
* read into locals before the constructor evaluates the other members,
* while Node evaluates the members in order, so every earlier member must
* evaluate without effects, and when the value itself has effects, every
* earlier member must also read nothing those effects could change. */
function earlierOptionsPure(
options: ts.ObjectLiteralExpression,
prop: ts.Node,
value: ts.Expression,
): boolean {
const valueHasEffects = !sideEffectFreeOptionValue(value) && !pureRead(value);
for (const member of options.properties.nodes) {
if (member === prop) return true;
if (ts.isPropertyAssignment(member)) {
const init = member.initializer;
if (
valueHasEffects
? !constantOptionValue(init)
: !sideEffectFreeOptionValue(init) && !pureRead(init)
)
return false;
} else if (ts.isShorthandPropertyAssignment(member)) {
if (valueHasEffects) return false;
} else if (ts.isSpreadAssignment(member)) return false;
}
return true;
}
/** The stream sides of a receiver class: the nearest stream-class
* ancestor's, or null off the stream hierarchy. */
export function streamSidesOf(
@@ -179,15 +234,15 @@ export const UNDERSCORE_METHODS: ReadonlyMap<string, string> = new Map([
/** The option callbacks applied after construction rather than through
* the constructor's callback slots (option name → method name): construct
* on every stream class, and writev on the writable classes whose
* machinery hands it batches (Writable and Duplex). */
* on every stream class, and writev on every class with a writable side
* (a Transform's batches reach writev, not the transform, as in Node). */
export const STREAM_EXTRA_CALLBACKS: ReadonlyMap<string, string> = new Map([
["construct", "_construct"],
["writev", "_writev"],
]);
export function streamExtraCallbacks(cls: string): readonly string[] {
return cls === "%Writable" || cls === "%Duplex" ? ["construct", "writev"] : ["construct"];
return cls === "%Readable" ? ["construct"] : ["construct", "writev"];
}
/** The completion callback's parameter position in each underscore
@@ -442,6 +497,13 @@ interface StreamOptions {
* right after construction (Node's constructors call addAbortSignal
* once the state exists), or null when absent. */
signal: IrExpr | null;
/** Non-literal decodeStrings and defaultEncoding values: valueDecls
* evaluates them into locals before construction (the defaultEncoding
* validated there, as the state constructors throw before the stream
* exists), and stream.optionValues applies them to the fresh stream. */
valueDecls: IrStmt[];
decodeStringsValue: IrExpr | null;
defaultEncodingValue: IrExpr | null;
/** The objectMode option's sides (a Duplex's readableObjectMode and
* writableObjectMode set one side): applied with stream.objectMode
* right after construction. */
@@ -732,6 +794,12 @@ function pinAnyDataCompletion(
if (!symbol || lowerer.stageSourceTypes.has(symbol)) return null;
const declared = lowerer.paramShape(param).type;
if (declared.kind !== "func" || declared.rest || declared.params[1]?.kind !== "dyn") return null;
// Only when every use calls the callback with a Buffer, a string or
// nothing as `data`: another value (or a callback that escapes) keeps
// `data` checked-dynamic, so the runtime pushes it as push(data) does
// and reports ERR_INVALID_ARG_TYPE through 'error', as Node does.
if (fnNode.body === undefined || !completionDataIsChunk(lowerer, fnNode.body, symbol, param.name))
return null;
const data = unionOf(lowerer, [BYTES, STRING, { kind: "undefinedT" }]);
lowerer.stageSourceTypes.set(
symbol,
@@ -740,6 +808,59 @@ function pinAnyDataCompletion(
return symbol;
}
/** Whether every reference to the completion callback `symbol` in `body`
* calls it with a `data` argument that is a Buffer, a string, null or
* undefined (or with no data). */
function completionDataIsChunk(
lowerer: Lowerer,
body: ts.Node,
symbol: ts.Symbol,
declaration: ts.Node,
): boolean {
let ok = true;
const visit = (node: ts.Node): void => {
if (!ok) return;
if (
ts.isIdentifier(node) &&
node !== declaration &&
lowerer.checker.getSymbolAtLocation(node) === symbol
) {
const call = node.parent;
if (!call || !ts.isCallExpression(call) || call.expression !== node) {
ok = false;
return;
}
const data = call.arguments.nodes[1];
if (data !== undefined) {
let t: IrType | null;
try {
t = lowerer.mapTypeOf(lowerer.typeOf(data));
} catch {
t = null;
}
const arms =
t?.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? null) : t ? [t] : null;
if (
arms === null ||
!arms.every(
(a) =>
(a.kind === "bytes" && a.elem === "u8") ||
a.kind === "string" ||
a.kind === "undefinedT" ||
a.kind === "nullT",
)
) {
ok = false;
return;
}
}
}
ts.forEachChild(node, visit);
};
visit(body);
return ok;
}
/** Parses the options object literal of a stream constructor. `which`
* names the callbacks this class accepts, in canonical flag order. */
function parseStreamOptions(
@@ -767,6 +888,9 @@ function parseStreamOptions(
badEncoding: null,
pushEncoding: null,
signal: null,
valueDecls: [],
decodeStringsValue: null,
defaultEncodingValue: null,
readableObject: false,
writableObject: false,
decodeStringsOff: false,
@@ -802,6 +926,21 @@ function parseStreamOptions(
optsLit !== undefined &&
ts.isObjectLiteralExpression(optsLit) &&
literalWritesValues(optsLit, ctorName !== "Readable" && ctorName !== "Writable");
// In JavaScript a decodeStrings value known only at run time may let
// string writes through, so the write and transform callbacks' chunk
// parameters read as any (the declarations type it Buffer).
const runtimeDecodeStrings =
optsLit !== undefined &&
isJsSourceFile(ts.getSourceFileOfNode(optsLit)) &&
ts.isObjectLiteralExpression(optsLit) &&
(optsLit.properties?.nodes ?? []).some(
(p) =>
ts.isPropertyAssignment(p) &&
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
p.name.text === "decodeStrings" &&
p.initializer.kind !== ts.SyntaxKind.TrueKeyword &&
p.initializer.kind !== ts.SyntaxKind.FalseKeyword,
);
if (out.readableObject) out.hwmR = numLit(16, loc);
if (out.writableObject) out.hwmW = numLit(16, loc);
const tuples = cbTuples(lowerer);
@@ -851,6 +990,7 @@ function parseStreamOptions(
}
const full = tuples[name]!.map((t) => (t === "fn" ? null : t));
const pinned = out.readableObject ? null : pinAnyDataCompletion(lowerer, name, fnNode);
let anyChunk: ts.Symbol | undefined;
try {
// The completion-callback tail: typed by the checker contextually;
// accept the user's declared func type as the tuple's entry so the
@@ -867,12 +1007,22 @@ function parseStreamOptions(
}
return declared ?? funcOf([], VOID);
});
const chunkParam =
runtimeDecodeStrings && (name === "write" || name === "transform")
? fnNode.parameters.nodes[0]
: undefined;
anyChunk =
chunkParam !== undefined && ts.isIdentifier(chunkParam.name) && !chunkParam.type
? lowerer.checker.getSymbolAtLocation(chunkParam.name)
: undefined;
if (anyChunk) lowerer.anySymbols.add(anyChunk);
present.set(
name,
lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, writesValues),
);
} finally {
if (pinned) lowerer.stageSourceTypes.delete(pinned);
if (anyChunk) lowerer.anySymbols.delete(anyChunk);
}
continue;
}
@@ -925,11 +1075,25 @@ function parseStreamOptions(
if (writableClass) out.decodeStringsOff = true;
break;
}
lowerer.noLowering(
`${ctorName} with a runtime 'decodeStrings' value`,
prop,
"write 'decodeStrings: true' or 'decodeStrings: false'",
);
// a runtime value: WritableState's `options.decodeStrings !== false`
if (
optsLit !== undefined &&
ts.isObjectLiteralExpression(optsLit) &&
!earlierOptionsPure(optsLit, prop, value)
)
lowerer.noLowering(
`the ${ctorName} option 'decodeStrings' with a runtime value that an earlier option value could observe or change`,
prop,
"compute the option values into consts before the options object (the runtime value is read before the other options run)",
);
const local = lowerer.declareHiddenLocal("%decodeStrings", DYN);
out.valueDecls.push({
kind: "varDecl",
localId: local.id,
init: lowerer.lowerExprExpecting(value, DYN),
loc,
});
if (writableClass) out.decodeStringsValue = varRef(local.id, DYN, loc);
break;
}
case "encoding": {
@@ -973,11 +1137,43 @@ function parseStreamOptions(
break;
}
}
lowerer.noLowering(
`the ${ctorName} option 'defaultEncoding' with a runtime value`,
prop,
"write the encoding as a string literal",
);
{
// a runtime value: null, undefined and utf8 are the default,
// another name Buffer.isEncoding knows is kept as given, and any
// other value throws ERR_UNKNOWN_ENCODING before construction
if (
optsLit !== undefined &&
ts.isObjectLiteralExpression(optsLit) &&
!earlierOptionsPure(optsLit, prop, value)
)
lowerer.noLowering(
`the ${ctorName} option 'defaultEncoding' with a runtime value that an earlier option value could observe or change`,
prop,
"compute the option values into consts before the options object (the runtime value is read before the other options run)",
);
const local = lowerer.declareHiddenLocal("%defaultEncoding", DYN);
const ref = varRef(local.id, DYN, loc);
out.valueDecls.push(
{
kind: "varDecl",
localId: local.id,
init: lowerer.lowerExprExpecting(value, DYN),
loc,
},
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "stream.checkEncodingOption",
args: [ref],
type: VOID,
loc,
},
loc,
},
);
out.defaultEncodingValue = ref;
}
break;
case "signal": {
// `if (options.signal) addAbortSignal(options.signal, this)`:
@@ -1041,9 +1237,7 @@ function parseStreamOptions(
lowerer.noLowering(
`the ${ctorName} option 'writev'`,
prop,
ctorName === "Readable"
? "a Readable has no writable side"
: "batched writes on a Transform would bypass its transform — writev is supported on Writable and Duplex",
"a Readable has no writable side",
);
break;
default:
@@ -1083,6 +1277,9 @@ function streamCtorArgs(
badEncoding: string | null;
pushEncoding: string | null;
signal: IrExpr | null;
valueDecls: IrStmt[];
decodeStringsValue: IrExpr | null;
defaultEncodingValue: IrExpr | null;
readableObject: boolean;
writableObject: boolean;
decodeStringsOff: boolean;
@@ -1117,6 +1314,9 @@ function streamCtorArgs(
badEncoding: o.badEncoding,
pushEncoding: o.pushEncoding,
signal: o.signal,
valueDecls: o.valueDecls,
decodeStringsValue: o.decodeStringsValue,
defaultEncodingValue: o.defaultEncodingValue,
readableObject: o.readableObject,
writableObject: o.writableObject,
decodeStringsOff: o.decodeStringsOff,
@@ -1237,6 +1437,18 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C
loc,
};
}
const values = optionValuesArgs(parsed, loc);
if (values) {
// runtime decodeStrings and defaultEncoding values (answers the
// receiver +1)
built = {
kind: "libCall",
fn: "stream.optionValues",
args: [built, ...values],
type: thisType,
loc,
};
}
if (parsed.pushEncoding !== null) {
// { defaultEncoding }: chain the push-side decode default (the
// setEncoding chaining shape — answers the receiver +1).
@@ -1269,8 +1481,11 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C
loc,
};
}
if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0)
return built;
if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0) {
return parsed.valueDecls.length > 0
? { kind: "seqExpr", stmts: parsed.valueDecls, result: built, type: thisType, loc }
: built;
}
const fresh = lowerer.declareHiddenLocal("%stream", thisType);
const followUps = streamOptionFollowUps(
parsed,
@@ -1280,13 +1495,30 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C
);
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: fresh.id, init: built, loc }, ...followUps],
stmts: [
...parsed.valueDecls,
{ kind: "varDecl", localId: fresh.id, init: built, loc },
...followUps,
],
result: varRef(fresh.id, thisType, loc),
type: thisType,
loc,
};
}
/** stream.optionValues' (decodeStrings, defaultEncoding) arguments, or
* null when both options are literals or absent. */
function optionValuesArgs(
parsed: { decodeStringsValue: IrExpr | null; defaultEncodingValue: IrExpr | null },
loc: SrcLoc,
): IrExpr[] | null {
if (parsed.decodeStringsValue === null && parsed.defaultEncodingValue === null) return null;
return [
parsed.decodeStringsValue ?? dynUndefinedExpr(loc),
parsed.defaultEncodingValue ?? dynUndefinedExpr(loc),
];
}
/** True when a value of Node's argument type `passed` widens implicitly
* into the declared parameter type `declared` (one of its union arms, or a
* sub-union of it). */
@@ -1589,6 +1821,7 @@ export function lowerStreamSuperCall(
];
}
const out: IrStmt[] = [
...parsed.valueDecls,
{
kind: "exprStmt",
expr: {
@@ -1614,6 +1847,20 @@ export function lowerStreamSuperCall(
loc,
});
}
const values = optionValuesArgs(parsed, loc);
if (values) {
out.push({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "stream.optionValues",
args: [thisRef(), ...values],
type: thisType,
loc,
},
loc,
});
}
if (parsed.pushEncoding !== null) {
out.push({
kind: "exprStmt",
@@ -1697,16 +1944,15 @@ export function lowerStreamUnderscoreAssign(
const loc = locOf(expr);
const cls = base.def.name;
const clsName = cls.slice(1);
if (methodName === "_construct") {
// Node's constructor reads _construct once; a later assignment has no
// effect there
lowerer.noLowering(
`assigning '${methodName}' on a ${clsName}`,
expr.left,
"declare _construct on the class or pass the construct option",
);
}
const which = methodName === "_writev" ? "writev" : optionByMethod.get(methodName)!;
// `_construct`: Node's constructNT reads it on the construction tick, so
// an assignment before that tick replaces a scheduled hook
// (stream.replaceConstruct) and has no effect after it or without one.
const which =
methodName === "_writev"
? "writev"
: methodName === "_construct"
? "construct"
: optionByMethod.get(methodName)!;
const { accepted } = streamCtorShape(cls);
if (!accepted.includes(which) && !streamExtraCallbacks(cls).includes(which)) {
lowerer.noLowering(
@@ -1745,6 +1991,7 @@ export function lowerStreamUnderscoreAssign(
transform: "stream.setTransform",
flush: "stream.setFlush",
writev: "stream.setWritev",
construct: "stream.replaceConstruct",
};
return {
kind: "exprStmt",
@@ -2699,6 +2946,22 @@ class StreamArgTypeThrow extends Error {
}
}
/** Whether a checked-dynamic pipeline callback can take undefined as its
* error: a callback that is already checked-dynamic, one without
* parameters, and one whose first parameter (or rest element) type holds
* undefined. */
function callbackErrorAdmitsUndefined(lowerer: Lowerer, cb: IrExpr): boolean {
if (cb.kind !== "dynFrom" || cb.value.type.kind !== "func") return true;
const fnT = cb.value.type;
const first = fnT.params[0];
if (first === undefined) return true;
const p0 =
fnT.rest === true && fnT.params.length === 1 && first.kind === "array" ? first.elem : first;
if (p0.kind === "dyn" || p0.kind === "jsval" || p0.kind === "undefinedT") return true;
if (p0.kind !== "union") return false;
return lowerer.unions.get(p0.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false;
}
/** The finished/pipeline completion callback: an inline function lowers
* through the option-callback machinery (leading `this`, an `Error |
* null` prefix — Node binds the stream and passes the error); any other
@@ -3160,12 +3423,16 @@ export function lowerStreamModuleCall(
// an async generator) reaches the callback through the
// checked-dynamic path.
const cb = lowerer.lowerExprExpecting(args[args.length - 1]!, DYN);
// SCR_PIPE_TYPED_CB: typed code's callback takes null for no error
// SCR_PIPE_TYPED_CB: a typed callback whose error parameter cannot
// hold undefined takes null for no error; others take undefined.
const stagedArgs = [...staged.args];
const flagsArg = stagedArgs[3]!;
if (flagsArg.kind !== "numLit")
throw new InternalCompilerError("pipeline flags not a literal");
if (!isJsSourceFile(ts.getSourceFileOfNode(call)))
if (
!isJsSourceFile(ts.getSourceFileOfNode(call)) &&
!callbackErrorAdmitsUndefined(lowerer, cb)
)
stagedArgs[3] = numLit(flagsArg.value | 128, loc);
return staged.wrap({
kind: "libCall",
@@ -503,7 +503,7 @@ import { lowerBuiltinExtraProperty } from "./builtins/properties.js";
import { registerPromisifiedBuiltinDecl } from "./builtins/callable-aliases.js";
import { lowerExecFileAsyncCall } from "./builtins/child-process-async.js";
import { lowerStringDecoderMethodCall, lowerTextCodecCall } from "./builtins/text-codecs.js";
import { lowerReadlineMethodCall } from "./builtins/readline.js";
import { isOptionalReadlineCursorRead, lowerReadlineMethodCall } from "./builtins/readline.js";
import {
lowerDiagnosticsChannelMethodCall,
lowerDiagnosticsChannelProperty,
@@ -5599,6 +5599,7 @@ export class Lowerer {
if (isIndexedRead(e)) return true;
if (ts.isPropertyAccessExpression(e) && isRecordIndexPropertyRead(e)) return true;
if (ts.isPropertyAccessExpression(e) && isOptionalProcessStreamProperty(this, e)) return true;
if (ts.isPropertyAccessExpression(e) && isOptionalReadlineCursorRead(this, e)) return true;
if (isOptionalProcessVersionsRead(this, e)) return true;
if (ts.isPropertyAccessExpression(e) && isSettledIteratorValueRead(this, e)) return true;
if (ts.isPropertyAccessExpression(e) && isOsConstantsSymbol(symbolOf(e.expression), this))
@@ -6,9 +6,11 @@
*
* A binding settles at the first statement of its hoisting scope (the
* function body or the source file) that stores it unconditionally: a
* direct `var x = e`, `x = e;`, or a loop declaring it (`for (var i = 0;
* ...)`, `for (var k in o)`), whose own condition, update and body run
* after the store. A read settles when it runs after that statement:
* direct `var x = e`, `x = e;`, or a `for` loop declaring it (`for (var
* i = 0; ...)`), whose own condition, update and body run after the store.
* A `for (var k in o)` or `for (var v of xs)` loop stores the binding only
* when it iterates, so it settles the reads in its body and no others. A
* read settles when it runs after the settling statement:
* - a read in the scope's own code, in a TypeScript file: tsc's
* definite-assignment analysis rejects any earlier one;
* - a read in the scope's own code, in a JavaScript file: its statement
@@ -194,6 +196,18 @@ function referencesBySymbol(
return out;
}
/** Where a binding is first stored unconditionally: the scope statement's
* index, and for a declarator of a `var` statement or a `for` initializer,
* the list and the declarator's end. */
interface Settle {
index: number;
list: ts.VariableDeclarationList | null;
end: number;
/** The first for-in/of loop declaring the binding before `index`, whose
* body runs after its store, or -1. */
loop: number;
}
class ScopeAnalysis {
private readonly js: boolean;
/** Whether a function declaration's references all settle, by scope
@@ -214,10 +228,13 @@ class ScopeAnalysis {
}
/** The index of the scope statement that first stores `symbol`
* unconditionally, or Infinity. */
settleIndex(scope: Scope, symbol: ts.Symbol): number {
* unconditionally (Infinity when none does), and, when that statement
* declares it in a `var` statement or a `for` initializer, the end of the
* storing declarator: later declarators of the list run after it. */
settleIndex(scope: Scope, symbol: ts.Symbol): Settle {
const statements = statementsOf(scope);
const checker = this.lowerer.checker;
let loopIndex = -1;
for (let i = 0; i < statements.length; i++) {
const s = statements[i]!;
const list = ts.isVariableStatement(s)
@@ -233,8 +250,10 @@ class ScopeAnalysis {
if (
(d.initializer !== undefined || loop) &&
boundIdentifiersOf(d.name).some((n) => checker.getSymbolAtLocation(n) === symbol)
)
return i;
) {
if (!loop) return { index: i, list, end: d.end, loop: loopIndex };
if (loopIndex < 0) loopIndex = i;
}
}
} else if (
ts.isExpressionStatement(s) &&
@@ -243,20 +262,21 @@ class ScopeAnalysis {
ts.isIdentifier(s.expression.left) &&
checker.getSymbolAtLocation(s.expression.left) === symbol
) {
return i;
return { index: i, list: null, end: -1, loop: loopIndex };
}
}
return Infinity;
return { index: Infinity, list: null, end: -1, loop: loopIndex };
}
/** Whether `ref` (inside `scope`, or in another module) runs only after
* the statement at index `settle`. `read`: `ref` reads the binding
* itself, so tsc vouches for one in the scope's own code. */
settles(scope: Scope, settle: number, ref: ts.Identifier, read = true): boolean {
* the store `settle` describes. `read`: `ref` reads the binding itself,
* so tsc vouches for one in the scope's own code. */
settles(scope: Scope, settle: Settle, ref: ts.Identifier, read = true): boolean {
if (ts.getSourceFileOfNode(ref) !== this.sf) return true;
const statements = statementsOf(scope);
let deferred: ts.Node | null = null;
let top: ts.Node | null = null;
const container: ts.Node | undefined = ts.isSourceFile(scope) ? scope : scope.body;
for (let n: ts.Node = ref; n.parent !== undefined && n !== scope; n = n.parent) {
if (
ts.isFunctionLike(n) ||
@@ -265,18 +285,26 @@ class ScopeAnalysis {
ts.isClassStaticBlockDeclaration(n)
)
deferred = n;
if (n.parent === scope || (!ts.isSourceFile(scope) && n.parent === scope.body)) top = n;
// The scope's own statement, not the function body that holds it.
if (n.parent === container) top = n;
}
if (top === null) return false;
if (deferred === null && read && !this.js) return true;
if (deferred !== null && ts.isFunctionDeclaration(deferred) && deferred.name !== undefined)
return this.functionSettles(scope, settle, deferred);
const index = statements.indexOf(top as ts.Statement);
return index > settle || (index === settle && settlesWithin(top, ref));
return (
index > settle.index ||
(index === settle.index &&
(settlesWithin(top, ref) ||
// A later declarator of the storing list (`var a = {}, b = { ...a }`).
(settle.list !== null && isInside(ref, settle.list) && ref.pos >= settle.end))) ||
(index === settle.loop && settlesWithin(top, ref))
);
}
private functionSettles(scope: Scope, settle: number, fn: ts.FunctionDeclaration): boolean {
const key = `${fn.pos}:${settle}`;
private functionSettles(scope: Scope, settle: Settle, fn: ts.FunctionDeclaration): boolean {
const key = `${fn.pos}:${settle.index}:${settle.end}:${settle.loop}`;
const known = this.functionMemo.get(key);
if (known !== undefined) return known;
this.functionMemo.set(key, true);
@@ -276,13 +276,14 @@ export default require_f();
).toBeNull();
});
test("an identifier-minified bundle names its lost helper names", () => {
// esbuild --minify output: the helper shapes survive, their names do not.
const result = split(
test("an identifier-minified bundle splits through its helper's shape", () => {
// esbuild --minify output: the helper shape survives, its name does not.
const answer = expectSameBehavior(
`var c=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);` +
`var x=c((t,m)=>{m.exports={v:1}});console.log(x().v);`,
`var x=c((t,m)=>{m.exports={v:1}});var y=c(t=>{t.w=x().v+1});` +
`console.log(x().v,y().w,x()===x());`,
);
expect(result).toEqual({ degrade: expect.stringContaining("identifier-minified") as string });
expect(answer.chunks.map((c) => basename(c.path))).toEqual(["index.js.x.cjs", "index.js.y.cjs"]);
});
test("an ES-module bundle of CommonJS chunks names the chunk interop", () => {
@@ -525,7 +525,7 @@ export function splitBundledChunks(
}
}
}
const helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS");
let helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS");
// Every name a recognizer relies on is written nowhere and declared
// once at the top level; `Object` itself is never rebound.
@@ -552,18 +552,14 @@ export function splitBundledChunks(
);
};
if (helperNames.length === 0) {
// The helper's shape under another name: an identifier-minified bundle,
// whose other interop helpers lost their names as well.
for (const [name, t] of top) {
// An identifier-minified bundle: the helper's shape under another name.
helperNames = [...top].flatMap(([name, t]) => {
const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined;
if (init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null)
return {
degrade:
"its shipped JS is an identifier-minified bundle whose bundler-interop helpers " +
"(__commonJS and the rest) have lost the names the recognizers verify",
};
}
return null;
return init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null
? [name]
: [];
});
if (helperNames.length === 0) return null;
}
const forms = new Map<string, HelperForm>();
for (const name of helperNames) {
@@ -260,11 +260,21 @@ function definePropertyExportOf(
value = getterReturnOf(p);
}
}
const esModuleStamp =
name === "__esModule" && value !== null && value.kind === ts.SyntaxKind.TrueKeyword;
const esModuleStamp = name === "__esModule" && value !== null && isTrueLiteral(value);
return { name, value, esModuleStamp };
}
/** `true`, or `!0`, its minified spelling. */
function isTrueLiteral(expr: ts.Expression): boolean {
return (
expr.kind === ts.SyntaxKind.TrueKeyword ||
(ts.isPrefixUnaryExpression(expr) &&
expr.operator === ts.SyntaxKind.ExclamationToken &&
ts.isNumericLiteral(expr.operand) &&
expr.operand.text === "0")
);
}
/** Space-pads [start, end) of `text` preserving newlines — the recognized
* statement disappears while every original offset survives. */
function padSpan(text: string, start: number, end: number): string {
+4 -21
View File
@@ -614,27 +614,12 @@ export function clearProgramBuiltinAliasCache(): void {
* but the bundler-emitted CJS export rewrite (npm-static-rewrite.ts)
* types the getter-table shapes and the offender attribution degrades a
* package whose surface still breaks its consumers — so those bundles are
* worth ATTEMPTING: eligible when the .d.ts and unminified criteria hold,
* gracefully per-package-degraded when the attempt fails. */
* worth ATTEMPTING: eligible when the .d.ts criteria hold, gracefully
* per-package-degraded when the attempt fails. Minified JS is attempted
* too: a minified dist is ordinary JavaScript, and the bundle recognizers
* know esbuild's __commonJS helper by its shape under any name. */
const TRANSFORM_MARKERS = ["__webpack_require__"];
/** Unminified-JS heuristic over an entry source: at least two lines and
* an average line length under 200 characters (minified dists are one
* enormous line; readable code averages well under 100). */
function looksUnminified(source: string): boolean {
const lines = source.split("\n");
if (lines.length < 2) return false;
return source.length / lines.length <= 200;
}
/** A whitespace/syntax-minified bundler dist whose interop helpers keep
* their names (`var __toESM=…,__toCommonJS=mod=>…`): the recognizers
* verify those shapes and refuse the package precisely when they cannot,
* so the minification heuristic need not refuse it up front. */
function namedBundlerHelpers(source: string): boolean {
return /\b__(?:toCommonJS|commonJS|toESM)\d*\s*=\s*(?:\(|[A-Za-z_$][\w$]*\s*=>)/.test(source);
}
function hasTransformMarkers(source: string): boolean {
return TRANSFORM_MARKERS.some((m) => source.includes(m));
}
@@ -676,8 +661,6 @@ export function npmStaticIneligibleReason(
if (jsEntry === null) return "no runtime JS entry resolves";
const source = trackedReadFile(jsEntry);
if (source === null) return `its runtime entry ${jsEntry} cannot be read`;
if (!looksUnminified(source) && !namedBundlerHelpers(source))
return "its shipped JS looks minified";
if (hasTransformMarkers(source)) {
return "its shipped JS carries build-transform markers (bundled/transpiled dist)";
}
+14 -2
View File
@@ -1438,8 +1438,20 @@ function requiresOf7(
return out;
}
/** `true`, or `!0`, its minified spelling. */
function isTrueLiteral(expr: ts.Expression): boolean {
return (
expr.kind === ts.SyntaxKind.TrueKeyword ||
(ts.isPrefixUnaryExpression(expr) &&
expr.operator === ts.SyntaxKind.ExclamationToken &&
ts.isNumericLiteral(expr.operand) &&
expr.operand.text === "0")
);
}
/** The exact `Object.defineProperty(exports|module.exports, "__esModule",
* { value: true })` interop stamp (shared by preflight and the no-op lowering). */
* { value: true })` interop stamp (shared by preflight and the no-op lowering);
* `{ value: !0 }` is the same stamp. */
export function isEsModuleStamp(expr: ts.Expression): boolean {
if (!ts.isCallExpression(expr) || expr.questionDotToken !== undefined) return false;
const callee = expr.expression;
@@ -1473,7 +1485,7 @@ export function isEsModuleStamp(expr: ts.Expression): boolean {
ts.isPropertyAssignment(p) &&
ts.isIdentifier(p.name) &&
p.name.text === "value" &&
p.initializer.kind === ts.SyntaxKind.TrueKeyword
isTrueLiteral(p.initializer)
);
}
@@ -380,6 +380,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"transform.newDyn",
"passthrough.newDyn",
"readable.initDyn",
"stream.checkEncodingOption",
"writable.initDyn",
"duplex.initDyn",
"transform.initDyn",
@@ -4037,6 +4037,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"stream.setTransform": { argTypes: [null, null], result: VOID },
"stream.setFlush": { argTypes: [null, null], result: VOID },
"stream.setConstruct": { argTypes: [null, null], result: VOID },
"stream.replaceConstruct": { argTypes: [null, null], result: VOID },
"stream.setWritev": { argTypes: [null, null], result: VOID },
"stream.setWritevDefault": { argTypes: [null, null], result: VOID },
/** The dyn-options twins (a checked-dynamic options record — the JS
@@ -4173,6 +4174,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* after construction; answers the receiver (+1, the chaining shape). */
"stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID },
"stream.decodeStrings": { argTypes: [null, BOOL], result: VOID },
/** Runtime decodeStrings and defaultEncoding option values, applied
* after construction (answers the receiver +1); the encoding was
* validated by stream.checkEncodingOption before construction. */
"stream.optionValues": { argTypes: [null, DYN, DYN], result: VOID },
"stream.checkEncodingOption": { argTypes: [DYN], result: VOID },
"stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID },
"stream.duplexFromDyn": { argTypes: [DYN], result: VOID },
/** Duplex.from over a lone Readable or Writable, or a { readable,
@@ -220,6 +220,8 @@ const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"stream.chunkDone",
"stream.objectMode",
"stream.decodeStrings",
"stream.optionValues",
"stream.checkEncodingOption",
"stream.duplexFromGen",
"stream.duplexFromDyn",
"stream.duplexFromPromise",
+3
View File
@@ -5500,6 +5500,7 @@ function validateFunction(
e.fn === "stream.destroyErr" ||
e.fn === "stream.objectMode" ||
e.fn === "stream.decodeStrings" ||
e.fn === "stream.optionValues" ||
e.fn === "stream.duplexFromGen" ||
e.fn === "stream.duplexFromDyn" ||
e.fn === "stream.duplexifyReadable" ||
@@ -5510,6 +5511,7 @@ function validateFunction(
e.fn === "stream.duplexFromGenSignal" ||
e.fn === "stream.duplexFromAsyncFnSignal" ||
e.fn === "stream.setConstruct" ||
e.fn === "stream.replaceConstruct" ||
e.fn === "stream.setWritev" ||
e.fn === "stream.setWritevDefault" ||
e.fn === "stream.iteratorClose" ||
@@ -5872,6 +5874,7 @@ function validateFunction(
e.fn === "readable.pushEncoding" ||
e.fn === "stream.objectMode" ||
e.fn === "stream.decodeStrings" ||
e.fn === "stream.optionValues" ||
e.fn === "writable.writeEncoding" ||
e.fn === "writable.setDefaultEncoding" ||
e.fn === "stream.destroy" ||
@@ -378,7 +378,7 @@ done:
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v36");
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v37");
const clangExe = join(dir, "clang-program");
const linkDriver = join(dir, "clang-driver.c");
@@ -712,7 +712,7 @@ done:
.split("\n");
expect(undefinedSymbols).toContain("_sf_scale");
expect(undefinedSymbols).toContain("_sf_callback_mix");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v36");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v37");
});
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7460-transform-writev.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7461-transform-writev.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7462-transform-writev.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7463-stream-option-values.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7464-stream-option-values.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7465-transform-callback-values.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7466-stream-construct-assign.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7500-pipeline-callback-undefined.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7501-pipeline-callback-undefined.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7502-readline-cursor-undefined.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7543-var-list-declarator-order.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7545-string-regex-checked-arguments.js"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs",
"<repo>/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs",
"<repo>/tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs",
"<repo>/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7553-var-for-in-of-zero-iterations.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7620-js-class-view-descriptors.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/stream-runtime-option-order.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts"
],
"diags": []
}
@@ -340,7 +340,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: RUNTIME_PACK_MATRIX.target,
runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" },
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
compiler: {
command: compiler,
identity: compilerVersion,
@@ -437,7 +437,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: matrix.target,
runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" },
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
compiler: {
command: compiler,
identity: compilerVersion,
+1 -1
View File
@@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) {
}
#endif
void scr_runtime_abi_v36(void) {}
void scr_runtime_abi_v37(void) {}
void scr_init(void) {
#ifdef SCR_WORKERS
+1 -1
View File
@@ -81,7 +81,7 @@ void scr_init(void);
/* Program objects emitted by the bundled LLVM helper reference this symbol.
* Its versioned spelling makes a mismatched manual runtime link fail before
* the program can start. */
void scr_runtime_abi_v36(void);
void scr_runtime_abi_v37(void);
void scr_runtime_workers_v8(void);
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
@@ -34,7 +34,7 @@
* receiver without a reference; the hook closures outlive the record.
*
* Not supported here (each throws the "not supported yet" Error where it
* runs): the options scr_stream.c's dyn option walk refuses, `_writev` and `_construct`, symbol event names, listeners() /
* runs): the options scr_stream.c's dyn option walk refuses, symbol event names, listeners() /
* rawListeners(), `wrap()`, and pipes into values that are neither runtime
* streams nor constructor-function streams. */
#include "scr_runtime.h"
@@ -898,20 +898,15 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t
}
bool failed = false;
bool writes = kind != BCS_READABLE;
bool transformish = kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH;
ScrDyn *opts = bcs_arg(args, argc, 0);
/* Node's constructors assign options.writev and options.construct to
* the instance (this._writev = options.writev), then read _writev and
* _construct, so an option shadows the prototype's method. */
if (opts->kind == SCR_DYN_OBJ) {
if (writes && !transformish && !bcs_copy_option(self, opts, "writev", "_writev")) return NULL;
if (writes && !bcs_copy_option(self, opts, "writev", "_writev")) return NULL;
if (!bcs_copy_option(self, opts, "construct", "_construct")) return NULL;
}
bool has_writev = writes && bcs_is_function(self, "_writev", &failed);
if (has_writev && transformish) {
bcs_unsupported("_writev on a constructor-function Transform is not supported yet");
return NULL;
}
bool has_construct = !failed && bcs_is_function(self, "_construct", &failed);
if (failed) return NULL;
ScrStream *s = kind == BCS_READABLE ? scr_stream_new_readable_dyn(opts)
+4
View File
@@ -3358,6 +3358,10 @@ static bool sf_header_value_ok(const ScrStr *value) {
}
static ScrStr *sf_header_value_bytestring(ScrStr *value) {
/* An ASCII value is its own ByteString: each byte is one UTF-16 unit. */
size_t ascii = 0;
while (ascii < value->len && (unsigned char)value->data[ascii] < 0x80) ascii++;
if (ascii == value->len) return scr_str_retain(value);
size_t len = (size_t)scr_str_utf16_len(value);
char *bytes = malloc(len ? len : 1);
if (!bytes) sf_oom();
+64 -9
View File
@@ -13976,19 +13976,74 @@ ScrDyn *scr_dyn_object_rest(const ScrDyn *source, const ScrDyn *excluded) {
/* Compiler-owned native class views retain every property and its flags,
* including nonenumerable data, without invoking accessors. */
ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) {
if (!src || src->kind == SCR_DYN_UNDEF) return scr_dyn_retain(target);
ScrDyn *keys = scr_dyn_obj_own_keys(src);
if (!keys) return NULL;
for (size_t i = 0; i < keys->v.arr.len; i++) {
ScrDyn *key = keys->v.arr.items[i];
ScrDyn *descriptor = scr_dyn_own_descriptor(src, key->v.str);
static void scr_dyn_copy_property_descriptor(ScrDyn *target, const ScrDyn *src,
const ScrDynEntry *from) {
/* Exactly scr_dyn_define_property(target, key, own descriptor of key)
* when `target` has no such property and accepts new ones: the entry
* takes the source's attributes, value or accessor pair. */
ScrDynEntry *prior = scr_dyn_find_entry(target, from->key, from->key_len);
if (prior || target->non_extensible) {
ScrStr *name = scr_str_new(from->key, from->key_len);
ScrDyn *key = scr_dyn_new_str(name);
ScrDyn *descriptor = scr_dyn_own_descriptor(src, name);
ScrDyn *result = descriptor ? scr_dyn_define_property(target, key, descriptor) : NULL;
scr_dyn_release(descriptor);
scr_dyn_release(result);
if (scr_exc_pending()) break;
scr_dyn_release(key);
scr_str_release(name);
return;
}
/* The names whose definition may move into a native Error's slots
* (scr_error_sync_slots). */
static const char *const slots[] = {"cause", "stack", "name", "message", "code"};
bool slot = false;
for (size_t i = 0; i < sizeof slots / sizeof *slots && !slot; i++)
slot = from->key_len == strlen(slots[i]) && memcmp(from->key, slots[i], from->key_len) == 0;
bool accessor = from->accessor, writable = !accessor && from->writable;
bool enumerable = from->enumerable, configurable = from->configurable;
ScrDyn *value = scr_dyn_retain(accessor ? scr_dyn_undefined() : from->value);
ScrDyn *getter = accessor && from->getter ? scr_dyn_retain(from->getter) : NULL;
ScrDyn *setter = accessor && from->setter ? scr_dyn_retain(from->setter) : NULL;
ScrStr *name = slot ? scr_str_new(from->key, from->key_len) : NULL;
scr_dyn_obj_set(target, from->key, from->key_len, value);
ScrDynEntry *entry = &target->v.obj.entries[target->v.obj.len - 1];
entry->accessor = accessor;
entry->getter = getter;
entry->setter = setter;
entry->writable = writable;
entry->enumerable = enumerable;
entry->configurable = configurable;
if (name) {
scr_error_sync_cause(target, name);
scr_str_release(name);
}
}
ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) {
if (!src || src->kind == SCR_DYN_UNDEF) return scr_dyn_retain(target);
if (src->kind == SCR_DYN_OBJ && target->kind == SCR_DYN_OBJ && src != target &&
src != scr_builtin_object_prototype && target != scr_builtin_error_prototype) {
/* Plain objects on both sides: no fence, proxy trap or accessor runs,
* so each property is copied from its entry without building the key
* and descriptor objects the general path goes through. */
if (src->v.obj.len) scr_dyn_region_store(target, NULL);
size_t *order = scr_dyn_obj_key_order(src);
for (size_t i = 0; i < src->v.obj.len && !scr_exc_pending(); i++)
scr_dyn_copy_property_descriptor(target, src, &src->v.obj.entries[order ? order[i] : i]);
free(order);
} else {
ScrDyn *keys = scr_dyn_obj_own_keys(src);
if (!keys) return NULL;
for (size_t i = 0; i < keys->v.arr.len; i++) {
ScrDyn *key = keys->v.arr.items[i];
ScrDyn *descriptor = scr_dyn_own_descriptor(src, key->v.str);
ScrDyn *result = descriptor ? scr_dyn_define_property(target, key, descriptor) : NULL;
scr_dyn_release(descriptor);
scr_dyn_release(result);
if (scr_exc_pending()) break;
}
scr_dyn_release(keys);
}
scr_dyn_release(keys);
if (!scr_exc_pending()) {
ScrDyn *symbols = scr_dyn_get_own_property_symbols(src);
if (!symbols) return NULL;
+83 -7
View File
@@ -269,6 +269,7 @@ struct ScrStreamState {
* destroyed and runs its _destroy once construction completes. */
bool constructing, destroy_deferred;
bool construct_finished; /* onConstruct ran: a second callback is ERR_MULTIPLE_CALLBACK */
bool construct_called; /* the construct tick called the hook */
ScrError *destroy_err; /* owned: the deferred destroy's error */
ScrClosure *construct_cb;
ScrStreamPlainInv construct_inv;
@@ -2157,6 +2158,24 @@ static ScrDyn *scr_stream_wentry_encoding(const ScrStreamState *st, ScrDyn *enc)
return scr_dyn_retain(enc ? enc : scr_dyn_undefined());
}
/* A compiled writev whose entries declare `encoding` a string: an entry
* without one (an object-mode write passes undefined or null) gets "".
* The batch array is the one this call receives. */
void scr_stream_writev_fill_encodings(ScrDyn *chunks) {
if (!chunks || chunks->kind != SCR_DYN_ARR) return;
double n = scr_dyn_arr_len(chunks);
for (double i = 0; i < n; i++) {
ScrDyn *entry = scr_dyn_arr_at(chunks, i); /* +1 */
ScrDyn *enc = entry->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(entry, "encoding", 8) : NULL;
if (entry->kind == SCR_DYN_OBJ && (!enc || enc->kind == SCR_DYN_UNDEF || enc->kind == SCR_DYN_NULL)) {
ScrStr *empty = scr_str_new("", 0);
scr_dyn_obj_set(entry, "encoding", 8, scr_dyn_new_str(empty));
scr_str_release(empty);
}
scr_dyn_release(entry);
}
}
/* A batch entry's callback: the write's callback, or Node's nop. */
static ScrDyn *scr_stream_writev_entry_cb_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
(void)args;
@@ -2675,9 +2694,8 @@ void *scr_stream_native(ScrStream *s, void (*release)(void *)) {
* way the compiler-emitted dyn thunks do (chunk as Buffer-flavored bytes,
* encoding 'buffer', the error via the boundary encoding, the completion
* callback as a callable dyn over the *_done glue). Options that Node's
* base WOULD consume but have no lowering here (objectMode truthy,
* decodeStrings:false, non-utf8 defaultEncoding, signal, construct,
* writev, and read/write/final on the transformish bases) throw the
* base WOULD consume but have no lowering here (signal, and
* read/write/final on the transformish bases) throw the
* "not supported yet" Error — the compile-time fence's runtime twin. */
/* The boxed dyn callable (+1 — scr_box_get_ref retains; callers release
@@ -2903,11 +2921,8 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy
}
}
out->construct = scr_stream_dynopt_fn(opts, "construct", 9);
/* a Transform's batches reach writev, not the transform, as in Node */
if (base != 'r') out->writev = scr_stream_dynopt_fn(opts, "writev", 6);
if (transformish && out->writev != NULL) {
static const char m[] = "the stream option 'writev' on a Transform is not supported yet (batches would bypass the transform)";
return scr_stream_dynopt_unsupported(m, sizeof m - 1);
}
{
/* a known encoding or ERR_UNKNOWN_ENCODING (the states' constructors) */
ScrDyn *v = scr_dyn_obj_get((ScrDyn *)opts, "defaultEncoding", 15);
@@ -6296,6 +6311,55 @@ ScrStream *scr_stream_set_default_encoding(ScrStream *s, ScrDyn *enc) {
return scr_stream_retain(s);
}
/* Whether a defaultEncoding option value is the default: null, undefined,
* 'utf8' or 'utf-8' (the state constructors' kDefaultUTF8Encoding). */
static bool scr_stream_default_encoding_value(const ScrDyn *v) {
if (!v || v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL) return true;
return v->kind == SCR_DYN_STR && ((v->v.str->len == 4 && memcmp(v->v.str->data, "utf8", 4) == 0) ||
(v->v.str->len == 5 && memcmp(v->v.str->data, "utf-8", 5) == 0));
}
/* A runtime defaultEncoding option value, checked before construction as
* the state constructors check it: the default, a name Buffer.isEncoding
* knows, or ERR_UNKNOWN_ENCODING (thrown). Borrows v. */
void scr_stream_check_encoding_option(ScrDyn *v) {
if (scr_stream_default_encoding_value(v)) return;
if (v->kind == SCR_DYN_STR) {
ScrStr *canonical = scr_bytes_normalize_encoding(v->v.str);
if (canonical) scr_str_release(canonical);
return;
}
ScrStr *shown = scr_dyn_string_coerce_js(v);
if (!shown) return;
char msg[128];
int len = snprintf(msg, sizeof msg, "Unknown encoding: %.*s", (int)(shown->len < 64 ? shown->len : 64),
shown->data);
scr_str_release(shown);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)(len < 0 ? 0 : len), "ERR_UNKNOWN_ENCODING");
}
/* Runtime decodeStrings and defaultEncoding option values over a fresh
* stream (after its objectMode): decodeStrings === false keeps string
* writes, and a known defaultEncoding is the write side's default as
* given and how a byte-mode readable side decodes push(string). Borrows
* both; answers the receiver (+1). */
ScrStream *scr_stream_option_values(ScrStream *s, ScrDyn *decode_strings, ScrDyn *default_encoding) {
ScrStreamState *st = s->st;
if (st->has_w && decode_strings && decode_strings->kind == SCR_DYN_BOOL && !decode_strings->v.b)
scr_stream_set_decode_strings(s, false);
if (!scr_stream_default_encoding_value(default_encoding) && default_encoding->kind == SCR_DYN_STR) {
ScrStr *canonical = scr_bytes_normalize_encoding(default_encoding->v.str);
if (canonical) {
scr_stream_set_write_encoding(s, default_encoding->v.str);
if (st->has_r && !st->r.object_entries) scr_stream_release(scr_stream_set_push_encoding(s, canonical));
scr_str_release(canonical);
} else if (scr_exc_pending()) {
scr_exc_clear(); /* checked before construction */
}
}
return scr_stream_retain(s);
}
/* stream.objectMode: scr_stream_set_object_mode, answering the receiver
* (+1). */
ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable) {
@@ -7460,6 +7524,17 @@ void scr_stream_set_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamP
scr_stream_construct(s, cb, inv);
}
void scr_stream_replace_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamPlainInv inv) {
ScrStreamState *st = s->st;
if (!st->constructing || st->construct_called || !st->construct_cb) {
scr_closure_release(cb);
return;
}
scr_closure_release(st->construct_cb);
st->construct_cb = cb;
st->construct_inv = inv;
}
/* A class's _writev under options that may carry their own writev (the
* option, an instance property in Node, wins). */
void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamWritevInv inv) {
@@ -7728,6 +7803,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) {
scr_stream_error_buffer(s);
break;
case SCR_ST_CONSTRUCT: {
st->construct_called = true;
if (st->construct_cb) {
st->construct_inv(st->construct_cb, s);
/* a throw inside _construct reaches onConstruct on a tick */
+12
View File
@@ -561,8 +561,15 @@ void scr_stream_set_writev_concat(ScrStream *s);
/* The user construct option / _construct method (scheduled now; cb
* moves), and the writev option / _writev method (cb moves). */
void scr_stream_set_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv);
/* `s._construct = fn` after construction: replaces a construct hook whose
* tick has not run yet (Node's constructNT reads _construct then), and has
* no effect otherwise (cb moves). */
void scr_stream_replace_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv);
void scr_stream_set_writev(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv);
void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv);
/* A compiled writev whose entries declare `encoding` a string: entries
* without an encoding (object mode) get "" before the conversion. */
void scr_stream_writev_fill_encodings(struct ScrDyn *chunks);
/* Duplex.from (duplexify.js): over an async generator function (the
* compiled stage takes the source generator that the compiled source
* makes from next() and ack(); all borrowed, helpers too), and over a
@@ -605,6 +612,11 @@ ScrStream *scr_stream_duplex_from_promise(struct ScrPromise *p, ScrClosure *box)
* forms answer the stream (+1). */
void scr_stream_set_decode_strings(ScrStream *s, bool decode);
ScrStream *scr_stream_decode_strings(ScrStream *s, bool decode);
/* Runtime decodeStrings/defaultEncoding option values: checked before
* construction (ERR_UNKNOWN_ENCODING thrown), then applied to the fresh
* stream, answering it (+1). Values borrowed. */
void scr_stream_check_encoding_option(struct ScrDyn *v);
ScrStream *scr_stream_option_values(ScrStream *s, struct ScrDyn *decode_strings, struct ScrDyn *default_encoding);
void scr_stream_set_write_encoding(ScrStream *s, ScrStr *enc);
ScrStream *scr_stream_write_encoding(ScrStream *s, ScrStr *enc);
ScrStream *scr_stream_set_default_encoding(ScrStream *s, struct ScrDyn *enc);
+86
View File
@@ -551,6 +551,91 @@ static void test_class_values(void) {
scr_str_release(name);
}
/* The class-view copy (scr_dyn_copy_property_descriptors) gives each
* property of the bag the attributes, value or accessors, and own-key
* position that defining it from its own descriptor would. */
static ScrDyn *define_from(ScrDyn *target, const char *key, ScrDyn *value, bool writable,
bool enumerable, bool configurable) {
ScrDyn *descriptor = scr_dyn_new_obj();
scr_dyn_obj_set(descriptor, "value", 5, value);
scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(writable));
scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(enumerable));
scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(configurable));
ScrStr *name = S(key);
ScrDyn *k = scr_dyn_new_str(name);
ScrDyn *result = scr_dyn_define_property(target, k, descriptor);
scr_dyn_release(k);
scr_str_release(name);
scr_dyn_release(descriptor);
return result;
}
static const ScrDynEntry *entry_of(const ScrDyn *obj, const char *key) {
for (size_t i = 0; i < obj->v.obj.len; i++)
if (obj->v.obj.entries[i].key_len == strlen(key) &&
memcmp(obj->v.obj.entries[i].key, key, strlen(key)) == 0)
return &obj->v.obj.entries[i];
return NULL;
}
static void test_copy_property_descriptors(void) {
ScrDyn *bag = scr_dyn_new_obj();
ScrDyn *getter = scr_dyn_new_obj();
scr_dyn_release(define_from(bag, "label", scr_dyn_new_num(1), true, true, true));
scr_dyn_release(define_from(bag, "hidden", scr_dyn_new_num(2), false, false, true));
scr_dyn_obj_set(bag, "7", 1, scr_dyn_new_num(7));
scr_dyn_obj_set(bag, "3", 1, scr_dyn_new_num(3));
/* An accessor entry (the getter object stands in for its function). */
scr_dyn_obj_set(bag, "computed", 8, scr_dyn_retain(scr_dyn_undefined()));
ScrDynEntry *accessor = (ScrDynEntry *)entry_of(bag, "computed");
accessor->accessor = true;
accessor->getter = scr_dyn_retain(getter);
accessor->writable = false;
accessor->enumerable = false;
accessor->configurable = false;
ScrDyn *view = scr_dyn_new_obj();
scr_dyn_release(scr_dyn_copy_property_descriptors(view, bag));
check(!scr_exc_pending() && view->v.obj.len == 5, "copy keeps every bag property");
ScrDyn *keys = scr_dyn_obj_own_keys(view);
const char *order[] = {"3", "7", "label", "hidden", "computed"};
bool ordered = keys->v.arr.len == 5;
for (size_t i = 0; ordered && i < 5; i++) ordered = str_is(keys->v.arr.items[i]->v.str, order[i]);
check(ordered, "copy keeps the bag's own-key order");
scr_dyn_release(keys);
const ScrDynEntry *hidden = entry_of(view, "hidden");
check(hidden && !hidden->accessor && hidden->value->v.num == 2 && !hidden->writable &&
!hidden->enumerable && hidden->configurable,
"copy keeps data attributes");
const ScrDynEntry *copied = entry_of(view, "computed");
check(copied && copied->accessor && copied->getter == getter && !copied->setter &&
!copied->writable && !copied->enumerable && !copied->configurable &&
copied->value->kind == SCR_DYN_UNDEF,
"copy keeps an accessor without calling it");
/* A property the target already has is redefined like defineProperty. */
ScrDyn *existing = scr_dyn_new_obj();
scr_dyn_obj_set(existing, "label", 5, scr_dyn_new_num(9));
scr_dyn_release(scr_dyn_copy_property_descriptors(existing, bag));
const ScrDynEntry *label = entry_of(existing, "label");
check(!scr_exc_pending() && existing->v.obj.len == 5 && existing->v.obj.entries[0].key_len == 5 &&
label->value->v.num == 1,
"copy redefines an existing property in place");
/* A target that takes no new properties throws as defineProperty does. */
ScrDyn *closed = scr_dyn_new_obj();
scr_dyn_release(scr_dyn_prevent_extensions(closed));
ScrDyn *refused = scr_dyn_copy_property_descriptors(closed, bag);
check(!refused && scr_exc_pending() && closed->v.obj.len == 0, "copy onto a non-extensible object throws");
scr_exc_clear();
scr_dyn_release(closed);
scr_dyn_release(existing);
scr_dyn_release(view);
scr_dyn_release(bag);
scr_dyn_release(getter);
}
static void test_primitive_owners(void) {
ScrDyn *values[] = {scr_dyn_new_null(), scr_dyn_new_bool(false), scr_dyn_new_bool(true)};
const char *texts[] = {"null", "false", "true"};
@@ -579,6 +664,7 @@ int main(void) {
test_class_values();
scr_init();
test_primitive_owners();
test_copy_property_descriptors();
/* ── primitives ─────────────────────────────────────────────────── */
ScrDyn *d = parse_ok("null", "parse null");
+33
View File
@@ -0,0 +1,33 @@
// writev on a Transform and a PassThrough (the writev option): a batch of
// buffered writes reaches writev instead of the transform, a lone write
// still reaches the transform, and the readable side gets what writev
// pushes.
import { Transform, PassThrough } from "node:stream";
const t = new Transform({
transform(chunk, enc, cb) { console.log("transform", String(chunk), enc); cb(null, chunk); },
writev(chunks, cb) {
console.log("writev", chunks.map((c) => `${c.chunk}/${c.encoding}`).join(","));
for (const { chunk } of chunks) this.push(String(chunk).toUpperCase());
cb();
},
});
t.on("data", (d) => console.log("data", String(d)));
t.on("finish", () => console.log("finish"));
t.on("end", () => console.log("end"));
t.write("a");
t.cork();
t.write("b");
t.write("c", "latin1");
process.nextTick(() => { t.uncork(); t.end("d"); });
const p = new PassThrough({
objectMode: true,
writev(chunks, cb) { console.log("pt writev", JSON.stringify(chunks.map((c) => c.chunk))); cb(); },
});
p.on("data", (d) => console.log("pt data", JSON.stringify(d)));
p.cork();
p.write({ x: 1 });
p.write([2]);
p.uncork();
p.write("solo");
p.end();
+33
View File
@@ -0,0 +1,33 @@
// Constructor-function Transform and PassThrough streams with _writev:
// the prototype's _writev takes the batches, and a writev option shadows
// it with the stream as this.
'use strict';
const { Transform, PassThrough } = require('node:stream');
function T(opts) { Transform.call(this, opts); }
Object.setPrototypeOf(T.prototype, Transform.prototype);
Object.setPrototypeOf(T, Transform);
T.prototype._transform = function (chunk, enc, cb) { cb(null, 'T(' + chunk + ')'); };
T.prototype._writev = function (chunks, cb) {
this.push('V(' + chunks.map((c) => String(c.chunk)).join('+') + ')');
cb();
};
const t = Object.create(T.prototype);
T.call(t, {});
t.on('data', (d) => console.log('t data', String(d)));
t.on('end', () => console.log('t end'));
t.write('a');
t.cork();
t.write('b');
t.write('c');
process.nextTick(() => { t.uncork(); t.end(); });
function P(opts) { PassThrough.call(this, opts); }
Object.setPrototypeOf(P.prototype, PassThrough.prototype);
Object.setPrototypeOf(P, PassThrough);
const p = Object.create(P.prototype);
P.call(p, { writev(chunks, cb) { console.log('p writev', chunks.length, this === p); cb(); } });
p.on('data', (d) => console.log('p data', String(d)));
p.cork();
p.write('x');
p.write('y');
p.uncork();
p.write('z');
+109
View File
@@ -0,0 +1,109 @@
// Typed writev on Transform and PassThrough: an options writev with
// decodeStrings: false and flush, a Transform subclass's _writev, a
// batch completing with an error, and object-mode entries whose
// encoding is declared as a string or as Node's string | null |
// undefined.
import { Transform, PassThrough, Writable } from "node:stream";
type WritevChunks = Array<{ chunk: any; encoding: BufferEncoding }>;
type Cb = (error?: Error | null) => void;
type TransformCallback = (error?: Error | null, data?: any) => void;
const log: string[] = [];
const t = new Transform({
decodeStrings: false,
transform(chunk: string, enc: BufferEncoding, cb: TransformCallback): void {
log.push(`transform ${chunk} ${enc}`);
cb(null, chunk);
},
flush(cb: TransformCallback): void {
log.push("flush");
cb(null, "!");
},
writev(chunks: WritevChunks, cb: Cb): void {
log.push(`writev ${chunks.map((c) => `${c.chunk}/${c.encoding}`).join(",")}`);
this.push(chunks.map((c) => String(c.chunk)).join(""));
cb();
},
});
t.on("data", (d: Buffer) => log.push(`data ${d.toString()}`));
t.on("end", () => log.push("end"));
t.on("finish", () => log.push("finish"));
t.on("close", () => {
console.log(log.join("\n"));
next();
});
t.write("one");
t.cork();
t.write("two", "latin1");
t.write("three");
t.uncork();
t.end("four");
class Batcher extends Transform {
_transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void {
console.log("batcher transform", chunk.toString());
cb(null, chunk);
}
_writev(chunks: Array<{ chunk: any; encoding: BufferEncoding }>, cb: Cb): void {
console.log("batcher writev", chunks.length);
this.push(Buffer.concat(chunks.map((c) => c.chunk as Buffer)));
cb();
}
}
function next(): void {
const b = new Batcher();
b.on("data", (d: Buffer) => console.log("batcher data", d.toString()));
b.write("x");
b.cork();
b.write("y");
b.write("z");
process.nextTick(() => {
b.uncork();
b.end();
});
b.on("finish", () => {
const p = new PassThrough({
writev(chunks: WritevChunks, cb: Cb): void {
console.log("pt writev", chunks.length, String(chunks[0]!.chunk));
cb(new Error("batch failed"));
},
});
p.on("error", (e: Error) => {
console.log("pt error", e.message);
objectEntries();
});
p.cork();
p.write("1");
p.write("2");
p.uncork();
});
}
function objectEntries(): void {
const declared = new Writable({
objectMode: true,
write(_chunk: any, _enc: BufferEncoding, cb: Cb): void { cb(); },
writev(chunks: WritevChunks, cb: Cb): void {
console.log("string encodings", JSON.stringify(chunks.map((c) => c.chunk)));
cb();
},
});
declared.write({ n: 0 });
declared.cork();
declared.write({ n: 1 });
declared.write({ n: 2 }, () => {});
declared.uncork();
const nodeTyped = new Writable({
objectMode: true,
write(_chunk: any, _enc: BufferEncoding, cb: Cb): void { cb(); },
writev(chunks: Array<{ chunk: any; encoding: string | null | undefined }>, cb: Cb): void {
console.log("node encodings", JSON.stringify(chunks.map((c) => [c.chunk, c.encoding === undefined ? "undefined" : c.encoding])));
cb();
},
});
nodeTyped.write({ n: 0 });
nodeTyped.cork();
nodeTyped.write({ n: 1 });
nodeTyped.write({ n: 2 }, () => {});
nodeTyped.uncork();
}
+58
View File
@@ -0,0 +1,58 @@
// Typed decodeStrings and defaultEncoding options whose values are known
// only at run time: decodeStrings === false keeps string writes, a known
// defaultEncoding is the write default as given and how push(string)
// decodes, null and utf8 are the default, an unknown name throws
// ERR_UNKNOWN_ENCODING before construction, and a subclass's super()
// options take them the same way.
import { Writable, Readable, Duplex } from "node:stream";
type Cb = (error?: Error | null) => void;
const flags: boolean[] = JSON.parse("[false, true]");
const encodings: Array<string | null> = JSON.parse('["hex", "latin1", null, "UTF8"]');
for (const decode of flags) {
const w = new Writable({
decodeStrings: decode,
defaultEncoding: encodings[0] as BufferEncoding,
write(chunk: any, enc: string, cb: Cb): void {
console.log("write", decode, typeof chunk, enc, String(chunk));
cb();
},
});
w.write("6869");
}
const r = new Readable({ defaultEncoding: encodings[1] as BufferEncoding, read(): void {} });
r.push("é");
r.push(null);
r.on("data", (d: Buffer) => console.log("readable", d.toString("hex")));
const dflt = new Writable({
defaultEncoding: encodings[2] as any,
write(chunk: Buffer, enc: string, cb: Cb): void {
console.log("null default", enc, chunk.toString());
cb();
},
});
dflt.write("ok");
const upper = new Duplex({
defaultEncoding: encodings[3] as BufferEncoding,
decodeStrings: flags[0],
read(): void {},
write(chunk: any, enc: string, cb: Cb): void {
console.log("duplex", typeof chunk, enc);
cb();
},
});
upper.write("x");
try {
new Writable({ defaultEncoding: JSON.parse('"nope"') });
} catch (e) {
console.log("bad", (e as Error & { code?: string }).code, (e as Error).message);
}
class Sink extends Writable {
constructor(decode: boolean, encoding: BufferEncoding) {
super({ decodeStrings: decode, defaultEncoding: encoding });
}
_write(chunk: any, enc: string, cb: Cb): void {
console.log("sink", typeof chunk, enc);
cb();
}
}
new Sink(flags[0]!, encodings[1] as BufferEncoding).write("y");
@@ -0,0 +1,28 @@
// Option values known only at run time in JavaScript: decodeStrings and
// defaultEncoding read from an object, a write callback whose chunk is
// then a string, decodeStrings: 0 (only false turns decoding off), a
// defaultEncoding that is not a string, and a _construct assigned after
// construction, which Node's constructor never reads.
import { Writable, Readable } from "node:stream";
const opts = { decode: false, enc: "base64" };
const w = new Writable({
decodeStrings: opts.decode,
defaultEncoding: opts.enc,
write(c, e, cb) { console.log("js", typeof c, e, String(c)); cb(); },
});
w.write("aGk=");
try { new Writable({ defaultEncoding: 5 }); } catch (e) { console.log("bad", e.code, e.message); }
const zero = new Writable({ decodeStrings: 0, write(c, e, cb) { console.log("zero", Buffer.isBuffer(c), e); cb(); } });
zero.write("z");
const late = new Writable({ write(chunk, enc, cb) { console.log("write", chunk.toString()); cb(); } });
let ran = false;
late._construct = (cb) => { ran = true; cb(); };
late.write("a");
late.end(() => console.log("ended, construct ran:", ran));
const r = new Readable({ read() {} });
const order = [];
(order.push("receiver"), r)._construct = (order.push("value"), function (cb) { console.log("never"); cb(); });
console.log(order.join(","));
r.push("x");
r.push(null);
r.on("data", (d) => console.log("data", d.toString()));
@@ -0,0 +1,41 @@
// A transform or flush callback passing a value that is not a Buffer or a
// string: the push reports Node's ERR_INVALID_ARG_TYPE through 'error'
// and destroys the stream, for option callbacks and _transform methods,
// while Buffer and string values take the typed path.
import { Transform } from "node:stream";
type TransformCallback = (error?: Error | null, data?: any) => void;
const t = new Transform({
transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void {
console.log("transform", chunk.toString());
cb(null, 42);
},
});
t.on("error", (e: Error & { code?: string }) => console.log("error", e.name, e.code, e.message));
t.on("data", (d: Buffer) => console.log("data", d.toString()));
t.on("close", () => console.log("close", t.destroyed));
console.log("write", t.write("a"));
const f = new Transform({
transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { cb(null, chunk); },
flush(cb: TransformCallback): void { cb(null, { tail: true }); },
});
f.on("error", (e: Error & { code?: string }) => console.log("flush error", e.code, e.message));
f.on("data", (d: Buffer) => console.log("flush data", d.toString()));
f.on("close", () => console.log("flush close"));
f.end("z");
class Bad extends Transform {
_transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void {
cb(null, chunk.length);
}
}
const bad = new Bad();
bad.on("error", (e: Error & { code?: string }) => console.log("error", e.code, e.message));
bad.on("close", () => console.log("close"));
bad.write("abc");
class Good extends Transform {
_transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void {
cb(null, chunk.toString().toUpperCase());
}
}
const good = new Good();
good.on("data", (d: Buffer) => console.log("good", d.toString()));
good.end("xyz");
@@ -0,0 +1,46 @@
// Assigning _construct after construction: Node reads it on the
// construction tick, so an assignment before that tick replaces the
// scheduled hook, and one after it, or on a stream without a hook, has no
// effect.
import { Readable, Writable } from "node:stream";
const w = new Writable({
construct(cb) {
console.log("original construct");
cb();
},
write(chunk, _enc, cb) {
console.log("write", chunk.toString());
cb();
},
});
w._construct = function (cb) {
console.log("replacement construct");
cb();
};
w.write("a");
w.end();
w.on("finish", () => {
w._construct = function (cb) {
console.log("late construct");
cb();
};
console.log("finish");
readable();
});
// A stream without a construct hook: the assignment has no effect.
function readable() {
const r = new Readable({
read() {
this.push("x");
this.push(null);
},
});
r._construct = function (cb) {
console.log("never");
cb();
};
r.on("data", (d) => console.log("data", d.toString()));
r.on("end", () => console.log("end"));
}
@@ -0,0 +1,120 @@
// pipeline() with an async function or async generator destination: a
// typed callback whose error parameter admits undefined receives
// undefined when the pipeline succeeds, as in Node, and the error when it
// fails.
import { pipeline, Readable } from "node:stream";
function source(): Readable {
return Readable.from(["a", "b", "c"]);
}
async function join(src: AsyncIterable<string>): Promise<string> {
let s = "";
for await (const c of src) s += c;
return s;
}
function optional(): Promise<void> {
return new Promise((resolve) => {
pipeline(source(), join, (err?: Error | null, value?: string) => {
console.log("optional", err, err === undefined, value);
resolve();
});
});
}
function declared(): Promise<void> {
return new Promise((resolve) => {
pipeline(
source(),
async function (src: AsyncIterable<string>) {
let n = 0;
for await (const c of src) n += c.length;
return n;
},
(err: Error | null | undefined, value?: number) => {
console.log("declared", err, typeof err, value);
resolve();
},
);
});
}
function unknownParam(): Promise<void> {
return new Promise((resolve) => {
pipeline(source(), join, (err: unknown, value: unknown) => {
console.log("unknown", err, value);
resolve();
});
});
}
function rest(): Promise<void> {
return new Promise((resolve) => {
pipeline(
source(),
async function* (src: AsyncIterable<string>) {
for await (const c of src) yield c.toUpperCase();
},
async function (src: AsyncIterable<string>) {
for await (const c of src) void c;
},
(...args: unknown[]) => {
console.log("rest", args.length, args);
resolve();
},
);
});
}
function generatorDestination(): Promise<void> {
return new Promise((resolve) => {
pipeline(
source(),
async function* (src: AsyncIterable<string>) {
for await (const c of src) yield c;
},
(err: Error | null | undefined, value: unknown) => {
console.log("generator", err, value);
resolve();
},
);
});
}
function named(): Promise<void> {
return new Promise((resolve) => {
function done(err: Error | null | undefined, value: string | undefined): void {
console.log("named", err, value);
resolve();
}
pipeline(source(), join, done);
});
}
function failing(): Promise<void> {
return new Promise((resolve) => {
pipeline(
source(),
async function (src: AsyncIterable<string>) {
for await (const c of src) if (c === "b") throw new Error("boom at " + c);
return "never";
},
(err?: Error | null, value?: string) => {
console.log("failing", err?.message, value);
resolve();
},
);
});
}
async function main(): Promise<void> {
await optional();
await declared();
await unknownParam();
await rest();
await generatorDestination();
await named();
await failing();
}
void main();
@@ -0,0 +1,22 @@
// pipeline() with an async function destination in JavaScript: the
// callback receives undefined and the destination's value on success.
import { pipeline, Readable } from "node:stream";
pipeline(
Readable.from(["x", "y"]),
async function (src) {
const out = [];
for await (const c of src) out.push(c);
return out.join("-");
},
(err, value) => {
console.log("callback", err, typeof err, value);
pipeline(
Readable.from(["z"]),
async function (src) {
for await (const c of src) void c;
},
(...args) => console.log("no value", args),
);
},
);
@@ -0,0 +1,28 @@
// rl.cursor answers undefined on an interface that is not a terminal and
// the cursor position on a terminal interface.
import * as readline from "node:readline";
import { PassThrough, Writable } from "node:stream";
const input = new PassThrough();
const rl = readline.createInterface({ input });
const c = rl.cursor;
console.log("cursor", c, typeof rl.cursor, rl.cursor === undefined);
console.log("arithmetic", rl.cursor + 1, Number.isNaN(rl.cursor * 2));
console.log("fallback", rl.cursor ?? -1, `${rl.cursor}`);
const interfaces = [rl];
function first(): readline.Interface {
return interfaces[0]!;
}
console.log("call receiver", first().cursor);
rl.close();
const sink = new Writable({
write(_chunk: Buffer, _enc, cb) {
cb();
},
});
const terminal = readline.createInterface({ input: new PassThrough(), output: sink, terminal: true });
terminal.write("abc");
console.log("terminal", terminal.cursor, typeof terminal.cursor, terminal.cursor + 1);
terminal.close();
@@ -0,0 +1,66 @@
// A `var` read in a later declarator of the statement that stores it runs
// after the store; a read in the same or an earlier declarator sees undefined.
var base = { a: 1, b: "x" },
more = { ...base, c: 2 },
keys = Object.keys({ ...more }).join(","),
first = base.a + 1;
console.log(JSON.stringify(more), keys, first);
function describeBase() {
return base.b + more.c;
}
var rules = { space: /\s+/, word: /\w+/ },
merged = { ...rules, digit: /\d/ },
described = describeBase();
console.log(Object.keys(merged).join(","), merged.word.test("ab"), described);
var table = { one: 1, two: 2 },
lookup = function (key) {
return table[key];
},
arrow = (key) => ({ ...table })[key];
console.log(lookup("one"), arrow("two"));
var readLater = function () {
return later;
},
early = readLater(),
later = { k: 1 },
after = readLater().k;
console.log(early, after);
var self = self ? 1 : 2;
console.log(self);
for (var i = 0, j = i + 10; i < 2; i++) console.log(i, j);
function nested() {
var inner = [1, 2, 3],
copy = [...inner, 4],
total = copy.reduce((sum, n) => sum + n, 0);
return { copy, total };
}
console.log(JSON.stringify(nested()));
function earlyInFunction() {
show("before");
var point = { n: 1 },
next = { m: point.n + 1 };
show("after");
function show(when) {
console.log(when, typeof point, point === undefined ? "none" : point.n, next === undefined ? "none" : next.m);
}
var check = () => console.log("check", items === undefined);
check();
var items = [1];
check();
}
earlyInFunction();
function loopLists() {
for (var k = 0, sum = k; k < 3; k++) sum += k;
var doubled = sum * 2,
again = doubled + 1;
return [sum, doubled, again];
}
console.log(JSON.stringify(loopLists()));
@@ -0,0 +1,89 @@
// String search, match and replace with untyped pattern values: a RegExp
// is used as is, and any other value is converted as Node converts it.
const rules = JSON.parse('{"other":{}}');
rules.other.space = /\s+/g;
rules.other.word = /o+/;
rules.other.text = "l";
rules.other.dot = ".";
rules.other.none = undefined;
rules.other.nul = null;
rules.other.one = /(l)(o)/;
rules.other.many = /l+/g;
const t = " hello world";
console.log(
t.search(rules.other.word),
t.search(rules.other.text),
t.search(rules.other.none),
t.search(rules.other.nul),
t.search(rules.other.space),
"a.b".search(rules.other.dot),
);
console.log(rules.other.space.lastIndex);
let conversions = 0;
rules.other.obj = {
toString() {
conversions++;
return "w";
},
};
console.log(t.search(rules.other.obj), t.match(rules.other.obj)[0], conversions);
const all = t.match(rules.other.many);
console.log(JSON.stringify(all), rules.other.many.lastIndex);
const pair = t.match(rules.other.one);
console.log(pair[0], pair[1], pair[2], pair.index, pair.length, pair.input === t);
const literal = t.match(rules.other.dot);
console.log(literal[0], literal.index, literal.length);
console.log(JSON.stringify(t.match(rules.other.none)), t.match(rules.other.zz));
rules.other.zz = "zz";
console.log(t.match(rules.other.zz));
function first(text, re) {
const m = text.match(re);
return m ? m[0] : "none";
}
console.log(first("abc", rules.other.dot), first("", rules.other.dot));
console.log(t.replace(rules.other.space, (m) => "[" + m.length + "]"));
console.log(t.replace(rules.other.word, (m, offset) => m.toUpperCase() + offset));
console.log(t.replace(rules.other.text, (m, offset, subject) => "<" + m + offset + subject.length + ">"));
console.log(t.replaceAll(rules.other.text, (m) => m.toUpperCase()));
console.log(t.replaceAll(rules.other.space, () => "_"));
console.log(t.replace(rules.other.one, (m, p1, p2, offset) => p1 + "|" + p2 + "|" + offset));
console.log(t.replace(rules.other.text, (m, offset, subject, extra) => typeof extra));
console.log(t.replace(rules.other.text, (m) => m.length));
try {
t.replaceAll(rules.other.word, (m) => m);
} catch (e) {
console.log(e.constructor.name, e.message);
}
function tabs(text, width) {
let column = 0;
let out = "";
for (const ch of text) {
if (ch === "\t") {
const pad = 4 - (column % 4);
out += " ".repeat(pad);
column += pad;
} else {
out += ch;
column++;
}
}
return out + width;
}
class Lexer {
constructor(table) {
this.rules = table;
}
indent(line, width) {
return line.replace(this.rules.leading, (w) => tabs(w, width));
}
firstText(line) {
return line.search(this.rules.nonSpace);
}
}
const lexer = new Lexer({ leading: /^( {1,4}| {0,3}\t)/, nonSpace: /[^ ]/ });
console.log(JSON.stringify(lexer.indent("\tabc", 2)), lexer.firstText(" x"));
@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: !0 });
exports.label = exports.describe = void 0;
const node_util_1 = require("node:util");
const node_path_1 = require("node:path");
function describe(name, size) {
return (0, node_util_1.format)("%s holds %d (%s)", name, size, (0, node_path_1.basename)("/a/b/c.txt"));
}
exports.describe = describe;
exports.label = "lib";
@@ -0,0 +1,19 @@
"use strict";
// tsc and esbuild output: the __esModule stamp spelled { value: !0 }, and
// calls of builtin module functions without a receiver, (0, ns.member)(...).
const lib = require("./lib.cjs");
const dc = require("node:diagnostics_channel");
const util = require("node:util");
const os = require("node:os");
const path = require("node:path");
console.log(lib.label, lib.describe("cache", 3));
const metrics = (0, dc.channel)("cache:metrics");
console.log(metrics.name, metrics.hasSubscribers);
const seen = [];
dc.subscribe("cache:metrics", (message) => seen.push(message.key));
metrics.publish({ key: "a" });
console.log(seen.join(","), metrics.hasSubscribers);
const tracing = (0, dc.tracingChannel)("cache");
console.log(tracing.hasSubscribers);
console.log((0, util.inspect)({ a: [1, 2] }), (0, util.isDeepStrictEqual)([1], [1]));
console.log((0, path.join)("a", "b"), typeof (0, os.platform)());
@@ -0,0 +1,7 @@
"use strict";
// Minifiers spell `true` as `!0`: Object.defineProperty(exports,
// "__esModule", { value: !0 }) is the same interop stamp as { value: true }.
const stamped = require("./stamped.cjs");
const moduleStamped = require("./module-stamped.cjs");
console.log(stamped.answer, stamped.twice(4), moduleStamped.label);
console.log(typeof stamped.twice, moduleStamped.label.length);
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(module.exports, "__esModule", { value: !0 });
module.exports.label = "module";
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: !0 });
exports.answer = void 0;
exports.answer = 42;
exports.twice = (n) => n * 2;
@@ -0,0 +1,18 @@
// A for-in or for-of loop stores its var binding only when it iterates:
// after a loop that ran zero times, closures read undefined.
function f(o) {
for (var key in o) {}
const read = () => typeof key;
const same = () => key === undefined;
return [read(), same()];
}
console.log(JSON.stringify(f({})), JSON.stringify(f({ a: 1 })));
function g(xs) {
for (var x of xs) {}
const show = () => [typeof x, String(x)];
return show();
}
console.log(JSON.stringify(g([])), JSON.stringify(g([4])));
for (var top in {}) {}
const readTop = () => typeof top;
console.log(readTop());
@@ -0,0 +1,91 @@
// An instance of a JS class with public fields keeps its own properties in
// a property bag, and untyped code that calls a method on it sees a view
// built from that bag: data properties with their attributes, accessors,
// index-like keys (which come first, in ascending order), symbols, and
// properties added or removed after construction. Every refresh of the view
// must copy the bag exactly.
'use strict';
class Box {
label = 'box';
count = 0;
constructor(n) {
this.count = n;
}
describe() {
return `${this.label}:${this.count}`;
}
bump() {
this.count++;
return this;
}
}
function call(target, method, ...args) {
return target[method](...args);
}
function put(target, key, value) {
target[key] = value;
}
function drop(target, key) {
delete target[key];
}
function show(title, target) {
const lines = [];
for (const key of Reflect.ownKeys(target)) {
const d = Object.getOwnPropertyDescriptor(target, key);
const kind = 'value' in d ? `value=${String(d.value)} writable=${d.writable}` : `get=${typeof d.get} set=${typeof d.set}`;
lines.push(` ${String(key)}: ${kind} enumerable=${d.enumerable} configurable=${d.configurable}`);
}
console.log(`${title}\n${lines.join('\n')}`);
}
const box = new Box(2);
Object.defineProperty(box, 'hidden', { value: 42, enumerable: false, writable: false, configurable: true });
Object.defineProperty(box, 'computed', {
get() {
return this.count * 10;
},
set(v) {
this.count = v / 10;
},
enumerable: true,
configurable: true,
});
Object.defineProperty(box, 'getterOnly', { get: () => 'g', enumerable: false, configurable: false });
put(box, 2, 'two');
put(box, 1, 'one');
put(box, 'extra', 'x');
const tag = Symbol('tag');
put(box, tag, 'symbol');
console.log(call(box, 'describe'));
show('after construction', box);
console.log('keys', Object.keys(box).join(','));
call(box, 'bump');
put(box, 'computed', 70);
drop(box, 'extra');
put(box, 'later', true);
console.log(call(box, 'describe'));
show('after changes', box);
console.log('computed', box.computed, 'hidden', box.hidden, 'getterOnly', box.getterOnly);
// Many views in a row see the same, current properties.
let total = 0;
for (let i = 0; i < 5; i++) {
call(box, 'bump');
total += Object.keys(box).length;
}
console.log('total keys', total, call(box, 'describe'));
const other = new Box(7);
console.log(call(other, 'describe'), Object.keys(other).join(','));
try {
call(box, 'missing');
} catch (error) {
console.log(error instanceof TypeError, error.message);
}
+1 -1
View File
@@ -5,7 +5,7 @@
import { describe } from "ni-gap";
import { compile } from "ni-eval";
import { Counter } from "ni-ctor";
import { shout } from "ni-minified";
import { shout } from "ni-bundled";
import { twice } from "ni-typed";
console.log(describe());
+15
View File
@@ -0,0 +1,15 @@
// A webpack bundle: its build-transform markers keep it out of the native
// subset as shipped (a packaging island).
var __webpack_modules__ = {
1: function (module) {
module.exports.shout = function shout(s) {
return s.toUpperCase() + "!";
};
},
};
function __webpack_require__(id) {
var module = { exports: {} };
__webpack_modules__[id](module);
return module.exports;
}
module.exports = __webpack_require__(1);
@@ -1,5 +1,5 @@
{
"name": "ni-minified",
"name": "ni-bundled",
"version": "1.0.0",
"main": "index.js"
}
@@ -1 +0,0 @@
"use strict";exports.shout=function shout(s){return s.toUpperCase()+"!"};
@@ -0,0 +1,42 @@
import { Writable } from "node:stream";
// Runtime decodeStrings and defaultEncoding values are read before the
// other option members are evaluated, while Node evaluates the members in
// order: a value after a member with effects refuses, and so does a value
// with effects after a member that reads a property those effects change.
let encoding = (process.env["STREAM_ENCODING"] ?? "utf8") as BufferEncoding;
let decode = process.env["STREAM_DECODE"] !== "0";
function bump(): number {
encoding = "hex";
decode = false;
return 16;
}
const sink = new Writable({
highWaterMark: bump(),
defaultEncoding: encoding,
write(_chunk, _encoding, done) {
done();
},
});
const other = new Writable({
highWaterMark: bump(),
decodeStrings: decode,
write(_chunk, _encoding, done) {
done();
},
});
const sizes = { hwm: 16 };
function pickEncoding(): BufferEncoding {
sizes.hwm = 1;
return "hex";
}
const third = new Writable({
highWaterMark: sizes.hwm,
defaultEncoding: pickEncoding(),
write(_chunk, _encoding, done) {
done();
},
});
sink.end();
other.end();
third.end();
@@ -0,0 +1,22 @@
// A one-line minified ES module. Its module-level var statement spreads
// earlier declarators into later ones, its tokenizer reads regexes through
// an untyped rules table (search, match and replace with callbacks), and
// one function receives both typed and untyped link objects.
import { render } from "gtminimark";
const doc = [
"# Title \\*one\\*",
"## Sub",
'[a](/x "T\\*")',
"[b](/y)",
'[r]: /ref "Ref"',
"[n]: /none",
"[R]",
"[n]",
"[z]",
"\tcode *em* line",
" three spaces *x* and *y*",
"plain words here",
].join("\n");
console.log(render(doc));
console.log(render(doc, { pedantic: true }));
@@ -0,0 +1,12 @@
// An identifier-minified esbuild CommonJS bundle (lru-cache's shape): its
// __commonJS helper and chunk parameters lost their names, the chunks
// stamp __esModule with { value: !0 }, and they call node:util and
// node:diagnostics_channel functions in the (0, ns.member)(...) form.
import { Cache } from "gtminicache";
const cache = new Cache<string, number>(2);
cache.set("a", 1).set("b", 2);
cache.get("a");
cache.set("c", 3);
console.log(cache.keys().join(","), cache.get("b"), cache.get("c"));
console.log(cache.describe("cache"));
+7
View File
@@ -0,0 +1,7 @@
export declare class Cache<K, V> {
constructor(max: number);
set(key: K, value: V): this;
get(key: K): V | undefined;
keys(): K[];
describe(label: string): string;
}
+1
View File
@@ -0,0 +1 @@
"use strict";var n=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports);var u=n(i=>{"use strict";Object.defineProperty(i,"__esModule",{value:!0});i.metrics=void 0;var o=require("node:diagnostics_channel");i.metrics=(0,o.channel)("gtminicache:metrics")});var d=n(r=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});r.describe=a;var l=require("node:util");function a(s,e){return(0,l.format)("%s holds %d",s,e)}});Object.defineProperty(exports,"__esModule",{value:!0});exports.Cache=void 0;var h=u(),_=d(),c=class{#e=new Map;#t;constructor(e){this.#t=e}set(e,t){for(this.#e.delete(e),this.#e.set(e,t);this.#e.size>this.#t;)this.#e.delete(this.#e.keys().next().value);return h.metrics.hasSubscribers&&h.metrics.publish({key:e}),this}get(e){let t=this.#e.get(e);return t!==void 0&&(this.#e.delete(e),this.#e.set(e,t)),t}keys(){return[...this.#e.keys()]}describe(e){return(0,_.describe)(e,this.#e.size)}};exports.Cache=c;
+7
View File
@@ -0,0 +1,7 @@
{
"name": "gtminicache",
"version": "1.0.0",
"description": "tsc-style CommonJS modules bundled by esbuild 0.27.0 with --bundle --platform=node --format=cjs --minify: the __commonJS helper and the chunk parameters lose their names, the __esModule stamps read { value: !0 }, and calls into node:util and node:diagnostics_channel use the (0, ns.member)(...) form",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
+1
View File
@@ -0,0 +1 @@
export declare function render(src: string, options?: { pedantic?: boolean }): string;
+1
View File
@@ -0,0 +1 @@
var m = { heading: /^(#{1,6}) +(.*)$/, link: /^\[([^\]]*)\]\(([^ )]*)(?: "([^"]*)")?\)/, ref: /^\[([^\]]*)\]$/, lead: /^( {1,4}| {0,3}\t)/, nonSpace: /[^ ]/, punct: /\\([\\*_\[\]])/g, word: /\w+/g, def: /^\[([^\]]*)\]: (\S+)(?: "([^"]*)")?$/ },B = { ...m, emph: /\*([^*]+)\*/g },P = { ...B, word: /[a-z]+/g },K = { normal: { other: m, inline: B }, pedantic: { other: m, inline: P, strict: !0 } };function X(a, e) { let t = 0, n = ""; for (let i of a) if (i === "\t") { let r = 4 - t % 4; n += " ".repeat(r), t += r } else n += i, t++; return n + e; }function Se(a, e, t, n) { let r = e.href, o = e.title || null; n.links++; return { type: "link", raw: t, href: r, title: o, text: a[1] }; }class T {constructor() { this.state = { links: 0 }; }heading(s) { let c = s.match(this.rules.other.heading); return c ? "h" + c[1].length + ":" + c[2].replace(this.rules.other.punct, "$1") : null; }indent(s, w) { return s.replace(this.rules.other.lead, W => X(W, w)); }first(s) { return s.search(this.rules.other.nonSpace); }emph(s) { return s.replace(this.rules.inline.emph, (W, x) => "<em>" + x + "</em>"); }words(s) { let c = s.match(this.rules.inline.word); return c ? c.length : 0; }link(s) { let c = this.rules.other.link.exec(s); if (!c) return null; let r = c[2], o = ""; o = c[3] ? c[3] : ""; return Se(c, { href: r && r.replace(this.rules.other.punct, "$1"), title: o && o.replace(this.rules.other.punct, "$1") }, c[0], this.state); }ref(s, l) { let c = this.rules.other.ref.exec(s); if (!c) return null; let o = l[c[1].toLowerCase()]; return o ? Se(c, o, c[0], this.state) : { type: "text", raw: c[0] }; }}class L {constructor(e) { this.options = e || {}; this.links = {}; this.tokenizer = new T(); this.tokenizer.rules = this.options.pedantic ? K.pedantic : K.normal; }define(s) { let c = s.match(this.tokenizer.rules.other.def); if (c) this.links[c[1].toLowerCase()] = { href: c[2], title: c[3] || "" }; return !!c; }line(s) {let t = this.tokenizer, h = t.heading(s);if (h) return h;let k = t.link(s);if (k) return JSON.stringify(k);let f = t.ref(s, this.links);if (f) return JSON.stringify(f);return t.first(s) + "|" + JSON.stringify(t.indent(s, t.first(s))) + "|" + t.emph(s) + "|" + t.words(s);}}export function render(src, options) {let l = new L(options), out = [];for (let s of src.split("\n")) if (!l.define(s)) out.push(l.line(s));return out.join("\n") + "\nlinks " + l.tokenizer.state.links;}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "gtminimark",
"version": "1.0.0",
"description": "A one-line minified ES module: a module-level var statement whose later declarators spread earlier ones, regexes read through an untyped rules table, and a function called with typed and untyped link objects",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
@@ -5,12 +5,12 @@ scriptc coverage tests/coverage-fixtures/node-islands/main.ts
in dynamic tier 17 (operate on values typed only at runtime; --explain-dynamic lists them)
npm package compilation:
ni-minified refused (its shipped JS looks minified)
ni-gap static
ni-eval static
ni-ctor static
ni-typed static
ni-dep static
ni-bundled refused (its shipped JS carries build-transform markers (bundled/transpiled dist))
ni-gap static
ni-eval static
ni-ctor static
ni-typed static
ni-dep static
Node islands (would-be, report-only: the build is unchanged; --node-islands=auto)
gap ni-gap@1.0.0
@@ -20,8 +20,8 @@ scriptc coverage tests/coverage-fixtures/node-islands/main.ts
object-model ni-ctor@1.0.0
tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.js:9:1: R2.1 constructor functions: a write of 'Counter.prototype.add'
+1 more reason in 1 module; carries ni-dep
packaging ni-minified@1.0.0
SC2013: its shipped JS looks minified
packaging ni-bundled@1.0.0
SC2013: its shipped JS carries build-transform markers (bundled/transpiled dist)
reached from native code and from an island: ni-typed
hint: scriptc coverage --explain-island=<package> lists every reason
@@ -31,9 +31,9 @@ scriptc coverage tests/coverage-fixtures/node-islands/main.ts
hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary)
blockers:
×2 SC2013 cannot compile npm package 'ni-minified' statically: its implementation is not part of the compiled module graph
×2 SC2013 cannot compile npm package 'ni-bundled' statically: its implementation is not part of the compiled module graph
at tests/coverage-fixtures/node-islands/main.ts:8:1, tests/coverage-fixtures/node-islands/main.ts:16:13
hint: use a dependency with supported source, or replace the unsupported operation; scriptc has no interpreter fallback
×1 SC2013 cannot compile npm package 'ni-minified' statically: its shipped JS looks minified
×1 SC2013 cannot compile npm package 'ni-bundled' statically: its shipped JS carries build-transform markers (bundled/transpiled dist)
at tests/coverage-fixtures/node-islands/main.ts:8:1
hint: to compile it anyway, name it with --npm-static=ni-minified (or --npm-static=ni-minified... to include its dependencies)
hint: to compile it anyway, name it with --npm-static=ni-bundled (or --npm-static=ni-bundled... to include its dependencies)
@@ -0,0 +1,26 @@
stream-runtime-option-order.ts:16:3 - error SC2020: 'the Writable option 'defaultEncoding' with a runtime value that an earlier option value could observe or change' is part of the standard library types but has no scriptc lowering yet
15 | highWaterMark: bump(),
16 | defaultEncoding: encoding,
| ^~~~~~~~~~~~~~~~~~~~~~~~~
17 | write(_chunk, _encoding, done) {
hint: compute the option values into consts before the options object (the runtime value is read before the other options run)
stream-runtime-option-order.ts:23:3 - error SC2020: 'the Writable option 'decodeStrings' with a runtime value that an earlier option value could observe or change' is part of the standard library types but has no scriptc lowering yet
22 | highWaterMark: bump(),
23 | decodeStrings: decode,
| ^~~~~~~~~~~~~~~~~~~~~
24 | write(_chunk, _encoding, done) {
hint: compute the option values into consts before the options object (the runtime value is read before the other options run)
stream-runtime-option-order.ts:35:3 - error SC2020: 'the Writable option 'defaultEncoding' with a runtime value that an earlier option value could observe or change' is part of the standard library types but has no scriptc lowering yet
34 | highWaterMark: sizes.hwm,
35 | defaultEncoding: pickEncoding(),
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
36 | write(_chunk, _encoding, done) {
hint: compute the option values into consts before the options object (the runtime value is read before the other options run)
@@ -0,0 +1 @@
{ "plain": 2266, "san": 2991 }
@@ -0,0 +1 @@
{ "plain": 1828, "san": 2413 }
@@ -0,0 +1 @@
{ "plain": 2178, "san": 2875 }
@@ -0,0 +1 @@
{ "plain": 1786, "san": 4192 }
@@ -0,0 +1 @@
{ "plain": 1678, "san": 4502 }
@@ -0,0 +1 @@
{ "plain": 1571, "san": 2074 }
@@ -0,0 +1 @@
{ "plain": 1571, "san": 2074 }

Some files were not shown because too many files have changed in this diff Show More