mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Merge remote-tracking branch 'origin/ni/i4-deferred' into HEAD
This commit is contained in:
@@ -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)`.
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7460-transform-writev.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7461-transform-writev.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7462-transform-writev.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7463-stream-option-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7464-stream-option-values.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7465-transform-callback-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7466-stream-construct-assign.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7500-pipeline-callback-undefined.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7501-pipeline-callback-undefined.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7502-readline-cursor-undefined.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7543-var-list-declarator-order.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7545-string-regex-checked-arguments.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -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": []
|
||||
}
|
||||
+8
@@ -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": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7553-var-for-in-of-zero-iterations.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7620-js-class-view-descriptors.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/stream-runtime-option-order.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
@@ -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');
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
Generated
Vendored
+15
@@ -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);
|
||||
Generated
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "ni-minified",
|
||||
"name": "ni-bundled",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js"
|
||||
}
|
||||
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export declare function render(src: string, options?: { pedantic?: boolean }): string;
|
||||
+1
@@ -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
@@ -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
Reference in New Issue
Block a user