diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c3c9dc3f..34c17aaeae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ Detailed changelog for Perry. See CLAUDE.md for concise summaries. +## v0.5.990 — feat(util/stream): util.inherits + stream prototype scaffold (express compile unblocked) + +**Symptom.** After PR #983 unblocked `safer-buffer`, `import express from 'express'` advanced one frame further and then crashed at `node_modules/send/index.js:173` with: + +``` +TypeError: Object prototype may only be an Object or null: undefined + at Object.setPrototypeOf () + at Object.inherits (node:util:14:52) + at file:.../send/index.js:356:6 +``` + +`send/index.js` does the classic pre-ES6 inheritance pattern: + +```js +var Stream = require('stream') // line 30 +function SendStream(req, path, options) { Stream.call(this); ... } +util.inherits(SendStream, Stream) // line 173 +``` + +`util.inherits(ctor, superCtor)` reads `superCtor.prototype` and calls `Object.setPrototypeOf(ctor.prototype, superCtor.prototype)`. Two coupled gaps converged on the crash: + +1. **`node:stream` default export was a plain namespace object.** The V8-fallback `node:stream` shim in `crates/perry-jsruntime/src/modules.rs` ended with `export default { Readable, Writable, Duplex, Transform, ... }` — a literal `{}` object, not the legacy `Stream` constructor Node ships. So `require('stream').prototype` was `undefined`. Even worse, the module had no `__perry_commonjs = true` marker, so the `wrap_commonjs` require() shim took the null-prototype-namespace branch and returned a flat copied object — `Stream.Readable` was preserved but `Stream.prototype` and `Stream` callability were not. + +2. **`util.inherits` was minimal.** The V8-fallback `node:util` `inherits` did only `Object.setPrototypeOf(ctor.prototype, superCtor.prototype)` — no `super_` static, no input validation, no useful error message when `superCtor` had no prototype. + +**Fix — three coordinated changes:** + +1. **`crates/perry-jsruntime/src/modules.rs` (`node:stream` shim):** rewrite the legacy `Stream` value as an actual class with its own `.prototype`, give it the EventEmitter-shaped instance methods (`on`/`once`/`emit`/`off`/`addListener`/`removeListener`/`removeAllListeners`/`pipe`), and re-root `Readable`/`Writable` to extend it. Attach `Readable`/`Writable`/`Duplex`/`Transform`/`PassThrough`/`pipeline`/`finished` as static properties on `Stream` itself, then `export default Stream`. Add `export const __perry_commonjs = true;` so `wrap_commonjs`'s `__perry_require_namespace` returns the class as-is from `require('stream')`. Also splits the old `"stream" | "stream/web"` match arm — they're now distinct, which silences the `unreachable_patterns` warning and lets each module evolve independently. + +2. **`crates/perry-jsruntime/src/modules.rs` (`node:util` shim):** harden `inherits(ctor, superCtor)` to mirror Node's contract: + - throw `TypeError` on null/undefined `ctor` or `superCtor`, + - throw `TypeError` if `superCtor.prototype` is undefined (matches Node's error message format), + - `Object.defineProperty(ctor, 'super_', { value: superCtor, writable: true, configurable: true })` so derived classes can chain through `Bar.super_` (the legacy Node pattern several stdlib packages use), + - then the original `Object.setPrototypeOf(ctor.prototype, superCtor.prototype)`. + +3. **`crates/perry-api-manifest/src/entries.rs`:** add `property("stream", "prototype")` to the manifest so the #463 unimplemented-API gate doesn't reject `Stream.prototype` reads in user TS. (`util.inherits` was already in the manifest.) + +**Validation.** +- `test-files/test_util_inherits.ts` + `test-files/fixtures/util_inherits_v8/inherits_mod.js` — V8-routed fixture mirroring the express/send pattern: `require('util')` + `require('stream')`, two `util.inherits()` calls, and assertions for `Stream.prototype`, `Stream.Readable.prototype`, `super_`, real-prototype-chain `instanceof`, and method resolution through the chain. All eight assertions pass under Perry. +- Express compile reproducer (`/tmp/perry-express-2/test.ts` = `import express from 'express'; console.log(typeof express)`) now succeeds: prints `function`. Previously this hit the `util.inherits` crash at module-init time and the namespace load failed. + +**Caveat.** The native-compile path for `util.inherits` (TS code that imports `node:util` directly without going through the V8 fallback) is still essentially a no-op — Perry's class-id `instanceof` and prototype model don't have a runtime hook for `util.inherits`. That's separate from this fix; users of the express/send chain are in the V8 fallback by definition (those packages are not in `compilePackages` and use CJS internally), so the V8-side change is what matters. + ## v0.5.989 — feat(runtime+jsruntime): expose Object.prototype methods on Buffer instances + plain-object require() namespace **Symptom.** `import express from 'express'` (and any transitive consumer of `safer-buffer`) threw `TypeError: buffer.hasOwnProperty is not a function` at module-init time. safer-buffer's `safer.js` opens with `for (key in buffer) { if (!buffer.hasOwnProperty(key)) continue; ... }` where `buffer` is the value returned by `require('buffer')`. Two distinct gaps converged on the same error message: diff --git a/CLAUDE.md b/CLAUDE.md index b4db67cc31..6122ab6cc0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.989 +**Current Version:** 0.5.990 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 9e3b20642f..75fc08f4a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4905,7 +4905,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "base64", @@ -4960,14 +4960,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.989" +version = "0.5.990" dependencies = [ "serde", ] [[package]] name = "perry-codegen" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "log", @@ -4980,7 +4980,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "perry-hir", @@ -4989,7 +4989,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "perry-hir", @@ -4997,7 +4997,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "perry-dispatch", @@ -5007,7 +5007,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "perry-hir", @@ -5016,7 +5016,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "base64", @@ -5029,7 +5029,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "perry-hir", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "perry-diagnostics" -version = "0.5.989" +version = "0.5.990" dependencies = [ "serde", "serde_json", @@ -5045,7 +5045,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.989" +version = "0.5.990" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5056,7 +5056,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "clap", @@ -5071,7 +5071,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.989" +version = "0.5.990" dependencies = [ "argon2", "perry-ffi", @@ -5079,7 +5079,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", "reqwest", @@ -5088,7 +5088,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.989" +version = "0.5.990" dependencies = [ "bcrypt", "perry-ffi", @@ -5096,7 +5096,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", "rusqlite", @@ -5104,7 +5104,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", "scraper", @@ -5112,14 +5112,14 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-cron" -version = "0.5.989" +version = "0.5.990" dependencies = [ "chrono", "cron", @@ -5128,7 +5128,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.989" +version = "0.5.990" dependencies = [ "chrono", "perry-ffi", @@ -5136,7 +5136,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", "rust_decimal", @@ -5144,7 +5144,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", "serde_json", @@ -5152,7 +5152,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -5160,21 +5160,21 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.989" +version = "0.5.990" dependencies = [ "bytes", "http-body-util", @@ -5188,7 +5188,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.989" +version = "0.5.990" dependencies = [ "lazy_static", "perry-ffi", @@ -5199,7 +5199,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.989" +version = "0.5.990" dependencies = [ "lazy_static", "perry-ext-http-server", @@ -5211,7 +5211,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.989" +version = "0.5.990" dependencies = [ "bytes", "http-body-util", @@ -5230,7 +5230,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.989" +version = "0.5.990" dependencies = [ "lazy_static", "perry-ffi", @@ -5240,7 +5240,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.989" +version = "0.5.990" dependencies = [ "base64", "jsonwebtoken", @@ -5251,7 +5251,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.989" +version = "0.5.990" dependencies = [ "lru", "perry-ffi", @@ -5259,7 +5259,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.989" +version = "0.5.990" dependencies = [ "chrono", "perry-ffi", @@ -5267,7 +5267,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.989" +version = "0.5.990" dependencies = [ "bson", "futures-util", @@ -5279,7 +5279,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.989" +version = "0.5.990" dependencies = [ "chrono", "perry-ffi", @@ -5289,7 +5289,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.989" +version = "0.5.990" dependencies = [ "nanoid", "perry-ffi", @@ -5298,7 +5298,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", "rustls", @@ -5309,7 +5309,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.989" +version = "0.5.990" dependencies = [ "lettre", "perry-ffi", @@ -5319,7 +5319,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", "sqlx", @@ -5328,7 +5328,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.989" +version = "0.5.990" dependencies = [ "governor", "perry-ffi", @@ -5336,7 +5336,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.989" +version = "0.5.990" dependencies = [ "base64", "image", @@ -5345,14 +5345,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.989" +version = "0.5.990" dependencies = [ "lazy_static", "perry-ffi", @@ -5360,7 +5360,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", "uuid", @@ -5368,7 +5368,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.989" +version = "0.5.990" dependencies = [ "perry-ffi", "regex", @@ -5378,7 +5378,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.989" +version = "0.5.990" dependencies = [ "futures-util", "lazy_static", @@ -5389,7 +5389,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.989" +version = "0.5.990" dependencies = [ "flate2", "perry-ffi", @@ -5397,7 +5397,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.989" +version = "0.5.990" dependencies = [ "dashmap 6.1.0", "once_cell", @@ -5406,7 +5406,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "perry-api-manifest", @@ -5420,7 +5420,7 @@ dependencies = [ [[package]] name = "perry-jsruntime" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "deno_core", @@ -5440,7 +5440,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "perry-diagnostics", @@ -5452,7 +5452,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "base64", @@ -5476,7 +5476,7 @@ dependencies = [ [[package]] name = "perry-stdlib" -version = "0.5.989" +version = "0.5.990" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -5546,7 +5546,7 @@ dependencies = [ [[package]] name = "perry-transform" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "perry-hir", @@ -5556,7 +5556,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.989" +version = "0.5.990" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -5564,11 +5564,11 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.989" +version = "0.5.990" [[package]] name = "perry-ui-android" -version = "0.5.989" +version = "0.5.990" dependencies = [ "itoa", "jni", @@ -5583,7 +5583,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.989" +version = "0.5.990" dependencies = [ "rand 0.8.6", "serde", @@ -5593,7 +5593,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.989" +version = "0.5.990" dependencies = [ "cairo-rs", "dirs 5.0.1", @@ -5612,7 +5612,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.989" +version = "0.5.990" dependencies = [ "block2", "libc", @@ -5627,7 +5627,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.989" +version = "0.5.990" dependencies = [ "block2", "libc", @@ -5645,11 +5645,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.989" +version = "0.5.990" [[package]] name = "perry-ui-tvos" -version = "0.5.989" +version = "0.5.990" dependencies = [ "block2", "libc", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.989" +version = "0.5.990" dependencies = [ "block2", "libc", @@ -5679,7 +5679,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.989" +version = "0.5.990" dependencies = [ "block2", "libc", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.989" +version = "0.5.990" dependencies = [ "libc", "perry-runtime", @@ -5706,7 +5706,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.989" +version = "0.5.990" dependencies = [ "base64", "ed25519-dalek", @@ -5720,7 +5720,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.989" +version = "0.5.990" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 2ad86cf858..5194eabd52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -190,7 +190,7 @@ opt-level = "s" # Optimize for size in stdlib opt-level = 3 [workspace.package] -version = "0.5.989" +version = "0.5.990" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 4d7ee0accd..a2c5d9d48b 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -1860,6 +1860,12 @@ pub static API_MANIFEST: &[ApiEntry] = &[ class("stream", "PassThrough"), method("stream", "pipeline", false, None), method("stream", "finished", false, None), + // `require('stream')` returns the legacy `Stream` constructor itself, + // which has its own `.prototype` (it extends EventEmitter). The + // `node_modules/send` package (express's static-file backend) does + // `util.inherits(SendStream, require('stream'))`, which reads + // `Stream.prototype` — the gate rejects the access without this entry. + property("stream", "prototype"), // `Readable.from(iterable)` — Node's static factory. Resolves // through the `Readable.foo` -> `stream.foo` route in // `lower_call.rs`, so the gate keys off `stream.from`. diff --git a/crates/perry-jsruntime/src/modules.rs b/crates/perry-jsruntime/src/modules.rs index 1404b14b2c..9ec85e0057 100644 --- a/crates/perry-jsruntime/src/modules.rs +++ b/crates/perry-jsruntime/src/modules.rs @@ -1067,19 +1067,45 @@ export function networkInterfaces() { return {}; } export const EOL = '\n'; export default { platform, arch, cpus, homedir, tmpdir, hostname, type, release, totalmem, freemem, uptime, loadavg, networkInterfaces, EOL }; "#.to_string(), - "stream" | "stream/web" => r#" -// Stub implementation for Node.js 'stream' module -export class Readable { + "stream" => r#" +// Stub implementation for Node.js 'stream' module. +// +// IMPORTANT: Node's `require('stream')` returns the legacy `Stream` +// *constructor* (a class) with `Readable`/`Writable`/etc. attached as +// static properties — NOT a plain namespace object. Packages like +// `send` / `express` rely on this shape: +// +// var Stream = require('stream') +// function SendStream() { Stream.call(this) } +// util.inherits(SendStream, Stream) // reads Stream.prototype +// +// If the default export is `{ Readable, Writable, ... }` then +// `Stream.prototype` is `undefined` and `util.inherits` blows up with +// "Object prototype may only be an Object or null: undefined". +// (See: node_modules/send/index.js:30,173.) +// +// So we make `Stream` a real class, attach the sub-classes as static +// properties, and export the *class itself* as default. +class Stream { constructor() {} - read() { return null; } + pipe(dest) { return dest; } on() { return this; } - pipe() { return this; } + once() { return this; } + emit() { return false; } + off() { return this; } + addListener() { return this; } + removeListener() { return this; } + removeAllListeners() { return this; } } -export class Writable { - constructor() {} +export class Readable extends Stream { + constructor() { super(); } + read() { return null; } + pipe(dest) { return dest; } +} +export class Writable extends Stream { + constructor() { super(); } write() { return true; } end() {} - on() { return this; } } export class Duplex extends Readable { write() { return true; } @@ -1092,7 +1118,25 @@ export class WritableStream {} export class TransformStream {} export function pipeline() {} export function finished() {} -export default { Readable, Writable, Duplex, Transform, PassThrough, ReadableStream, WritableStream, TransformStream, pipeline, finished }; +// Attach sub-classes as static properties so `Stream.Readable`, +// `Stream.Writable`, etc. resolve the way Node ships them. +Stream.Readable = Readable; +Stream.Writable = Writable; +Stream.Duplex = Duplex; +Stream.Transform = Transform; +Stream.PassThrough = PassThrough; +Stream.pipeline = pipeline; +Stream.finished = finished; +export { Stream }; +// `__perry_commonjs = true` tells the wrap_commonjs() require() shim in +// modules.rs to return `module.default` instead of the ESM namespace +// when this module is `require()`'d. Node's `require('stream')` returns +// the Stream class itself (with `.Readable` / `.prototype` / etc), +// NOT a namespace object. Without this flag, `var Stream = +// require('stream')` ends up as a copied null-proto object and +// `Stream.prototype` becomes undefined → `util.inherits` crashes. +export const __perry_commonjs = true; +export default Stream; "#.to_string(), "repl" => r#" // Stub implementation for Node.js 'repl' module @@ -1157,7 +1201,26 @@ export function format(fmt, ...args) { return fmt; } export function formatWithOptions(_inspectOptions, fmt, ...args) { return format(fmt, ...args); } export function debuglog() { return () => {}; } export function deprecate(fn) { return fn; } -export function inherits(ctor, superCtor) { Object.setPrototypeOf(ctor.prototype, superCtor.prototype); } +// `util.inherits(ctor, superCtor)` — Node's pre-class inheritance helper. +// Real Node semantics: +// Object.defineProperty(ctor, 'super_', { value: superCtor }); +// Object.setPrototypeOf(ctor.prototype, superCtor.prototype); +// Throws TypeError if either arg is missing a `.prototype`. We mirror that +// contract so packages like `send` (which derives `SendStream` from +// `require('stream')`) work transparently. +export function inherits(ctor, superCtor) { + if (ctor === undefined || ctor === null) { + throw new TypeError('The constructor to "inherits" must not be null or undefined'); + } + if (superCtor === undefined || superCtor === null) { + throw new TypeError('The super constructor to "inherits" must not be null or undefined'); + } + if (superCtor.prototype === undefined) { + throw new TypeError('The super constructor to "inherits" must have a prototype'); + } + Object.defineProperty(ctor, 'super_', { value: superCtor, writable: true, configurable: true }); + Object.setPrototypeOf(ctor.prototype, superCtor.prototype); +} export const TextEncoder = globalThis.TextEncoder; export const TextDecoder = globalThis.TextDecoder; // util.types — Node's runtime introspection namespace. NestJS / rxjs diff --git a/test-files/fixtures/util_inherits_v8/inherits_mod.js b/test-files/fixtures/util_inherits_v8/inherits_mod.js new file mode 100644 index 0000000000..b6db4c3d03 --- /dev/null +++ b/test-files/fixtures/util_inherits_v8/inherits_mod.js @@ -0,0 +1,32 @@ +// V8-fallback module exercising util.inherits + the legacy +// `require('stream')` Stream constructor. Mirrors the +// `node_modules/send/index.js` pattern that broke express compile +// before stream.prototype + util.inherits-super_ were scaffolded. +// +// This file lives outside compilePackages, so Perry routes it through +// the QuickJS / V8 fallback module loader — same path express's deps +// take in production. + +var util = require('util'); +var Stream = require('stream'); + +function SendStream() {} +util.inherits(SendStream, Stream); + +function Foo() {} +Foo.prototype.greet = function () { + return 'hello'; +}; +function Bar() {} +util.inherits(Bar, Foo); + +module.exports = { + typeofStream: typeof Stream, + typeofStreamProto: typeof Stream.prototype, + typeofStreamReadable: typeof Stream.Readable, + typeofStreamReadableProto: typeof Stream.Readable.prototype, + sendStreamSuper: SendStream.super_ === Stream, + barInstanceofFoo: new Bar() instanceof Foo, + barGreet: new Bar().greet(), + barSuper: Bar.super_ === Foo, +}; diff --git a/test-files/test_util_inherits.ts b/test-files/test_util_inherits.ts new file mode 100644 index 0000000000..d7c4971c55 --- /dev/null +++ b/test-files/test_util_inherits.ts @@ -0,0 +1,21 @@ +// Test coverage for util.inherits + node:stream prototype scaffold +// (fix for express → send "Object prototype may only be an Object or +// null" crash). The actual `util.inherits` exists only in the V8 +// fallback's `node:util` shim, and the bug surfaced via CJS +// `require('stream') / require('util')` in +// `node_modules/send/index.js:30,173`. So we route the assertion +// through a `.js` fixture (V8 fallback) and just import its summary. + +import results from './fixtures/util_inherits_v8/inherits_mod.js'; + +console.log('typeof Stream:', results.typeofStream); +console.log('typeof Stream.prototype:', results.typeofStreamProto); +console.log('typeof Stream.Readable:', results.typeofStreamReadable); +console.log( + 'typeof Stream.Readable.prototype:', + results.typeofStreamReadableProto, +); +console.log('SendStream.super_ === Stream:', results.sendStreamSuper); +console.log('Bar instanceof Foo:', results.barInstanceofFoo); +console.log('Bar.greet():', results.barGreet); +console.log('Bar.super_ === Foo:', results.barSuper);