From c82630e8a7d1e2a48bb50c13a965ef49c384a057 Mon Sep 17 00:00:00 2001 From: Nathan Whitaker Date: Tue, 28 Apr 2026 23:44:43 +0000 Subject: [PATCH] fix(ext/node): add emitExperimentalWarning/pendingDeprecate to internal/util, support modifyPrototype option in util.deprecate --- ext/node/polyfills/internal/util.mjs | 47 ++++++++++++++++++++++++++++ ext/node/polyfills/util.ts | 34 ++++++++++++++------ tests/node_compat/config.jsonc | 2 ++ 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/ext/node/polyfills/internal/util.mjs b/ext/node/polyfills/internal/util.mjs index 9a5a5b8ec7c1ab..b6966cfbe8d1a1 100644 --- a/ext/node/polyfills/internal/util.mjs +++ b/ext/node/polyfills/internal/util.mjs @@ -22,6 +22,7 @@ const { ObjectDefineProperty, ObjectFreeze, ObjectGetPrototypeOf, + ObjectGetOwnPropertyDescriptor, ObjectGetOwnPropertyDescriptors, ObjectPrototypeIsPrototypeOf, ObjectSetPrototypeOf, @@ -167,6 +168,50 @@ export function convertToValidSignal(signal) { const codesWarned = new SafeSet(); +const experimentalWarnings = new SafeSet(); + +export function emitExperimentalWarning(feature, messagePrefix, code, ctor) { + if (SetPrototypeHas(experimentalWarnings, feature)) return; + SetPrototypeAdd(experimentalWarnings, feature); + let msg = + `${feature} is an experimental feature and might change at any time`; + if (messagePrefix) { + msg = messagePrefix + msg; + } + globalThis.process.emitWarning(msg, "ExperimentalWarning", code, ctor); +} + +const pendingCodesWarned = new SafeSet(); + +// Internal deprecator for pending --pending-deprecation. Emits the warning only +// when --pending-deprecation is set and --no-deprecation is not. +export function pendingDeprecate(fn, msg, code) { + function deprecated(...args) { + const process = globalThis.process; + if ( + process.execArgv?.includes("--pending-deprecation") && + !process.noDeprecation + ) { + if (code !== undefined) { + if (!SetPrototypeHas(pendingCodesWarned, code)) { + process.emitWarning(msg, "DeprecationWarning", code, deprecated); + SetPrototypeAdd(pendingCodesWarned, code); + } + } else { + process.emitWarning(msg, "DeprecationWarning", deprecated); + } + } + return ReflectApply(fn, this, args); + } + + ObjectDefineProperty(deprecated, "length", { + __proto__: null, + ...ObjectGetOwnPropertyDescriptor(fn, "length"), + }); + + return deprecated; +} + export function deprecateInstantiation(Constructor, deprecationCode, ...args) { if (!SetPrototypeHas(codesWarned, deprecationCode)) { SetPrototypeAdd(codesWarned, deprecationCode); @@ -230,11 +275,13 @@ export default { customInspectSymbol, customPromisifyArgs, deprecateInstantiation, + emitExperimentalWarning, isError, kEmptyObject, kEnumerableProperty, normalizeEncoding, once, + pendingDeprecate, promisify, removeColors, sleep, diff --git a/ext/node/polyfills/util.ts b/ext/node/polyfills/util.ts index efc95f2a54ea2c..d25ce3d8107035 100644 --- a/ext/node/polyfills/util.ts +++ b/ext/node/polyfills/util.ts @@ -16,6 +16,7 @@ const { NumberPrototypeToString, ObjectCreate, ObjectDefineProperty, + ObjectGetOwnPropertyDescriptor, ObjectKeys, ObjectSetPrototypeOf, ReflectApply, @@ -201,8 +202,16 @@ const codesWarned = new SafeSet(); // Mark that a method should not be used. // Returns a modified function which warns once by default. // If --no-deprecation is set, then it is a no-op. -// deno-lint-ignore no-explicit-any -export function deprecate(fn: any, msg: string, code?: any) { +export function deprecate( + // deno-lint-ignore no-explicit-any + fn: any, + msg: string, + // deno-lint-ignore no-explicit-any + code?: any, + { modifyPrototype = true }: { __proto__: null; modifyPrototype?: boolean } = { + __proto__: null, + }, +) { process ??= lazyLoadProcess(); if (process.noDeprecation === true) { return fn; @@ -233,13 +242,20 @@ export function deprecate(fn: any, msg: string, code?: any) { return ReflectApply(fn, this, args); } - // The wrapper will keep the same prototype as fn to maintain prototype chain - ObjectSetPrototypeOf(deprecated, fn); - if (fn.prototype) { - // Setting this (rather than using Object.setPrototype, as above) ensures - // that calling the unwrapped constructor gives an instanceof the wrapped - // constructor. - deprecated.prototype = fn.prototype; + if (modifyPrototype) { + // The wrapper will keep the same prototype as fn to maintain prototype chain + ObjectSetPrototypeOf(deprecated, fn); + if (fn.prototype) { + // Setting this (rather than using Object.setPrototype, as above) ensures + // that calling the unwrapped constructor gives an instanceof the wrapped + // constructor. + deprecated.prototype = fn.prototype; + } + + ObjectDefineProperty(deprecated, "length", { + __proto__: null, + ...ObjectGetOwnPropertyDescriptor(fn, "length"), + }); } return deprecated; diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc index f2269892f2bfcf..173ff77864bcf6 100644 --- a/tests/node_compat/config.jsonc +++ b/tests/node_compat/config.jsonc @@ -3240,6 +3240,8 @@ "parallel/test-utf8-scripts.js": {}, "parallel/test-util-convert-signal-to-exit-code.mjs": {}, "parallel/test-util-deprecate-invalid-code.js": {}, + "parallel/test-util-deprecate.js": {}, + "parallel/test-util-emit-experimental-warning.js": {}, "parallel/test-util-getcallsites-preparestacktrace.js": {}, "parallel/test-util-getcallsites.js": {}, "parallel/test-util-inherits.js": {},