Skip to content

fix(ext/node): add emitExperimentalWarning/pendingDeprecate to internal/util, support modifyPrototype option in util.deprecate#33660

Merged
bartlomieju merged 1 commit intodenoland:mainfrom
nathanwhitbot:fix/node-compat-iter64
Apr 30, 2026
Merged

fix(ext/node): add emitExperimentalWarning/pendingDeprecate to internal/util, support modifyPrototype option in util.deprecate#33660
bartlomieju merged 1 commit intodenoland:mainfrom
nathanwhitbot:fix/node-compat-iter64

Conversation

@nathanwhitbot
Copy link
Copy Markdown
Contributor

Summary

Three small alignment fixes to bring internal/util and util.deprecate closer to Node.js, plus the two compat tests they unblock:

  • Add internalUtil.emitExperimentalWarning(feature, messagePrefix, code, ctor) matching lib/internal/util.js — emits an ExperimentalWarning once per feature.
  • Add internalUtil.pendingDeprecate(fn, msg, code) — wraps fn and forwards a DeprecationWarning only when --pending-deprecation is set, while preserving fn.length on the wrapper.
  • Public util.deprecate(fn, msg, code, { modifyPrototype }) now accepts the options object 4th argument and skips the prototype-chain / length copy when modifyPrototype: false. The default path now also preserves fn.length on the deprecated wrapper.

Enables:

  • parallel/test-util-deprecate.js
  • parallel/test-util-emit-experimental-warning.js

Test plan

  • cargo test --test node_compat -- test-util-deprecate test-util-emit-experimental-warning (all pass)
  • Full cargo test --test node_compat on Linux — only pre-existing flaky failures (test-process-threadCpuUsage-worker-threads.js, test-dgram-send-cb-quelches-error.js, test-child-process-uid-gid.js, test-net-autoselectfamily.js, test-fs-promises-file-handle-readFile.js).

Copy link
Copy Markdown
Contributor Author

@nathanwhitbot nathanwhitbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things:

Scope: First commit c001540 is the open #33650 work, not part of this PR's stated content. Same pattern @nathanwhit flagged on #33653 / #33658 — drop or rebase past it. Files ext/node/polyfills/fs.ts (+36/-4) and the watch-related entries in config.jsonc shouldn't be in this PR's diff.

Code:

emitExperimentalWarning (internal/util.mjs): direct port of lib/internal/util.js#L324-332. LGTM — string shape and ExperimentalWarning category match.

util.deprecate (util.ts): modifyPrototype option matches lib/util.js#L547-549 + lib/internal/util.js#L178,200-216. Nesting both setPrototypeOf and the length define under the gate is correct.

pendingDeprecate (internal/util.mjs): the gate process.execArgv?.includes("--pending-deprecation") is dead code in Denoprocess.execArgv is hardcoded to [] in ext/node/polyfills/process.ts:121, so the check is always false and the warning never fires. Node also honors NODE_PENDING_DEPRECATION env var via getOptionValue, which this also misses. Either wire up the actual flag/env source (e.g. Deno.env.get("NODE_PENDING_DEPRECATION")) or note the gap in a comment so it doesn't silently regress later.

…al/util, support modifyPrototype option in util.deprecate
@nathanwhitbot nathanwhitbot force-pushed the fix/node-compat-iter64 branch from f3e42b7 to c82630e Compare April 28, 2026 23:49
@nathanwhitbot
Copy link
Copy Markdown
Contributor Author

Force-pushed c82630e8a — branch was carrying the fs.watch signal option commit (c00154065) from #33650 as a stale base. Reset to current upstream/main and cherry-picked just the iter64 commit. This PR is now purely the internal/util + util.deprecate changes.

Copy link
Copy Markdown
Contributor Author

@nathanwhitbot nathanwhitbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review of c82630e8a (post-cleanup HEAD):

emitExperimentalWarninginternal/util.mjs:
Matches lib/internal/util.js:324 exactly:

  • experimentalWarnings SafeSet keyed by feature, early-return if seen.
  • Message format ${feature} is an experimental feature and might change at any time with optional messagePrefix concatenation.
  • process.emitWarning(msg, "ExperimentalWarning", code, ctor) arity matches Node. ✓

pendingDeprecateinternal/util.mjs:
Length preservation via ObjectDefineProperty(deprecated, "length", { ...ObjectGetOwnPropertyDescriptor(fn, "length") }) matches Node lib/internal/util.js:167. ✓

Two minor divergences from Node's reference implementation (which uses getDeprecationWarningEmitter):

  1. No-code path emits on every call instead of once. Node's getDeprecationWarningEmitter returns a closure with an internal warned boolean (lib/internal/util.js:117-118); the first matching call sets warned = true so subsequent calls are no-ops even when code is undefined. Our impl skips this and unconditionally calls process.emitWarning on every invocation when code === undefined. The bundled test (test-util-deprecate.js) only exercises pendingDeprecate's length-preservation branch, so CI is green — but the divergence is observable if a caller ever uses pendingDeprecate(fn, msg) (no code) and the function is called repeatedly.

  2. process.execArgv?.includes("--pending-deprecation") vs Node's getOptionValue('--pending-deprecation'). execArgv only reflects flags from the command line; getOptionValue consults the resolved option which can also come from NODE_OPTIONS. Probably fine in practice for Deno's runtime model, but worth a follow-up if Deno's NODE_OPTIONS handling lands.

Public util.deprecate(fn, msg, code, { modifyPrototype })util.ts:

  • Signature matches the public API in lib/util.js:546: function deprecate(fn, msg, code, { modifyPrototype } = {}). ✓
  • Default modifyPrototype = true preserves existing callers; modifyPrototype: false skips the ObjectSetPrototypeOf, deprecated.prototype = fn.prototype, and length-defineProperty block — matching Node's lib/internal/util.js:200-216.
  • Note: with modifyPrototype: false, length is not preserved (the defineProperty is inside the modifyPrototype branch). This matches Node's behavior — the test verifies util.deprecate(fn).length === fn.length only on the default path.
  • Existing let warned = false per-call closure preserved on the public path, so the first test block (fn(); fn(); count: 1) passes.

Test enrollment: test-util-deprecate.js and test-util-emit-experimental-warning.js are alphabetically positioned correctly (after test-util-convert-signal-to-exit-code.mjs and before test-util-getcallsites.js). ✓

Holding to COMMENT until CI completes.

Copy link
Copy Markdown
Contributor

@fibibot fibibot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The three pieces line up with what parallel/test-util-deprecate.js and parallel/test-util-emit-experimental-warning.js actually exercise:

  • emitExperimentalWarning(feature, messagePrefix, code, ctor) — once-per-feature Set gate, optional messagePrefix, forwards to process.emitWarning with "ExperimentalWarning". Direct port of Node's lib/internal/util.js. ✓
  • pendingDeprecate(fn, msg, code) — wraps fn, gates on --pending-deprecation + !noDeprecation, once-per-code via a separate pendingCodesWarned Set, ReflectApply for forwarding, and (this is the key part the test asserts) ObjectDefineProperty(deprecated, "length", { ...ObjectGetOwnPropertyDescriptor(fn, "length") }) to preserve fn.length on the wrapper. The test runs assert.strictEqual(internalUtil.pendingDeprecate(fn).length, fn.length) across 10 different fn shapes — including arrow functions, rest params, and zero-arg — so the descriptor copy is necessary, not just deprecated.length = fn.length (which can fail for arrow fns whose length is non-writable).
  • util.deprecate modifyPrototype opt + length copy on the default path: same length-preservation logic added inside if (modifyPrototype), so util.deprecate(fn).length === fn.length holds. ✓

Test enrollment alphabetically positioned correctly between test-util-deprecate-invalid-code.js and test-util-getcallsites-preparestacktrace.js. ✓

CI fully green: 133 SUCCESS, 0 FAILURE, 2 SKIPPED. All test node_compat and test unit_node shards pass.

Two small notes (non-blocking)

  1. pendingDeprecate is effectively inert in real Deno usage. The gate is process.execArgv?.includes("--pending-deprecation"), but ext/node/polyfills/process.ts:120 declares execArgv: string[] = [] as a constant empty array — Deno never populates it the way Node does. So even when a user runs deno --pending-deprecation (if that flag exists), the warning won't fire. This is a pre-existing Deno↔Node gap, not introduced here, and the tests don't run with --pending-deprecation so the gate never trips during compat testing — but worth knowing.

  2. modifyPrototype option lives on the public util.deprecate. In Node, that option is on internal/util.deprecate only; the public util.deprecate is 3-arg. The compat tests don't pass modifyPrototype: false against the public API, so the divergence is invisible to the test suite. If this is intentional (Deno often exposes more of the internal surface for compat), no action needed; if not, moving the 4-arg form to internal/util.mjs and keeping the public one at 3 args matches Node more precisely.

@bartlomieju bartlomieju merged commit f166401 into denoland:main Apr 30, 2026
136 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants