From 101febf59c5ab698f526f5143960be7e793a146f Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 6 May 2026 14:59:08 +0200 Subject: [PATCH 01/60] doc: remove list of versions in `BUILDING.md` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Antoine du Hamel PR-URL: https://github.com/nodejs/node/pull/63113 Reviewed-By: Filip Skokan Reviewed-By: René Reviewed-By: Michaël Zasso Reviewed-By: Marco Ippolito Reviewed-By: James M Snell Reviewed-By: Rafael Gonzaga Reviewed-By: Trivikram Kamat Reviewed-By: Luigi Pinca Reviewed-By: Paolo Insogna --- BUILDING.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 23d047563a64a7..b0f3387d93c0f2 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -214,12 +214,11 @@ If compiling without one of the above, use `configure` with the ### Previous versions of this document Supported platforms and toolchains change with each major version of Node.js. -This document is only valid for the current major version of Node.js. -Consult previous versions of this document for older versions of Node.js: +This document is only valid for the current version of Node.js, and is expected +to be valid for the entire lifetime of this release line. -* [Node.js 24](https://github.com/nodejs/node/blob/v24.x/BUILDING.md) -* [Node.js 22](https://github.com/nodejs/node/blob/v22.x/BUILDING.md) -* [Node.js 20](https://github.com/nodejs/node/blob/v20.x/BUILDING.md) +To consult the version of this document for another version, download its source +tarball and/or browse the git repository checked out at the relevant tag. ## Building Node.js on supported platforms From ddd9a8cb6c73a6a5fa2b44b9cd5089c5a7f7bec5 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Wed, 29 Apr 2026 12:18:20 +0200 Subject: [PATCH 02/60] doc: document the latest-vX.x schema Signed-off-by: Marco Ippolito PR-URL: https://github.com/nodejs/node/pull/63033 Reviewed-By: Pietro Marchini Reviewed-By: James M Snell --- doc/api/cli.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index ca5126bfd2c09c..6eeb360419df83 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -974,7 +974,8 @@ added: v23.10.0 If present, Node.js will look for a configuration file at the specified path. Node.js will read the configuration file and apply the settings. The configuration file should be a JSON file with the following structure. `vX.Y.Z` -in the `$schema` must be replaced with the version of Node.js you are using. +in the `$schema` must be replaced with the version of Node.js you are using or +`latest-vX.x` for the latest version of that major release line. ```json { From 797669530f43331bbe18c1c82ba64a7af51cece4 Mon Sep 17 00:00:00 2001 From: Anshika Jain Date: Thu, 7 May 2026 00:19:27 +0530 Subject: [PATCH 03/60] doc: add Hmac.digest() documentation-only deprecation (DEP0206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: anshikakalpana PR-URL: https://github.com/nodejs/node/pull/63121 Refs: https://github.com/nodejs/node/issues/62838 Reviewed-By: René Reviewed-By: Filip Skokan Reviewed-By: James M Snell --- doc/api/deprecations.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 9de12ceab5bbe3..2b136308fd3843 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -4439,6 +4439,22 @@ that have proven unresolveable. See [caveats of asynchronous customization hooks `module.registerHooks()` as soon as possible as `module.register()` will be removed in a future version of Node.js. +### DEP0206: Calling `digest()` on an already-finalized `Hmac` instance + + + +Type: Documentation-only + +Calling `hmac.digest()` more than once returns an empty buffer instead of +throwing an error. This behavior is inconsistent with `hash.digest()` and +may lead to subtle bugs. Calling `hmac.digest()` on a finalized `Hmac` instance +will throw an error in a future version. + [DEP0142]: #dep0142-repl_builtinlibs [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf [RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3 From 75143dabcda58b4ddeba6e300a706f7fca313cbe Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 7 May 2026 01:09:55 +0200 Subject: [PATCH 04/60] sqlite: keep source database alive during backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/62673 Reviewed-By: Daniel Lemire Reviewed-By: Tobias Nießen Reviewed-By: Edy Silva Reviewed-By: James M Snell --- src/node_sqlite.cc | 16 +++++++++-- test/parallel/test-sqlite-backup.mjs | 41 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 6965d0a3fa97a5..ecdcd9177f70b4 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -565,8 +565,10 @@ class BackupJob : public ThreadPoolWork { TryCatch try_catch(env()->isolate()); USE(fn->Call(env()->context(), Null(env()->isolate()), 1, argv)); if (try_catch.HasCaught()) { + Local exception = try_catch.Exception(); Finalize(); - resolver->Reject(env()->context(), try_catch.Exception()).ToChecked(); + resolver->Reject(env()->context(), exception).ToChecked(); + delete this; return; } } @@ -585,11 +587,15 @@ class BackupJob : public ThreadPoolWork { resolver ->Resolve(env()->context(), Integer::New(env()->isolate(), total_pages)) .ToChecked(); + delete this; } void Finalize() { Cleanup(); - source_->RemoveBackup(this); + if (source_) { + source_->RemoveBackup(this); + source_.reset(); + } } void Cleanup() { @@ -610,28 +616,32 @@ class BackupJob : public ThreadPoolWork { Local e; if (!CreateSQLiteError(env()->isolate(), dest_).ToLocal(&e)) { Finalize(); + delete this; return; } Finalize(); resolver->Reject(env()->context(), e).ToChecked(); + delete this; } void HandleBackupError(Local resolver, int errcode) { Local e; if (!CreateSQLiteError(env()->isolate(), errcode).ToLocal(&e)) { Finalize(); + delete this; return; } Finalize(); resolver->Reject(env()->context(), e).ToChecked(); + delete this; } Environment* env() const { return env_; } Environment* env_; - DatabaseSync* source_; + BaseObjectPtr source_; Global resolver_; Global progressFunc_; sqlite3* dest_ = nullptr; diff --git a/test/parallel/test-sqlite-backup.mjs b/test/parallel/test-sqlite-backup.mjs index 519555479642e0..80061ee6601d72 100644 --- a/test/parallel/test-sqlite-backup.mjs +++ b/test/parallel/test-sqlite-backup.mjs @@ -1,3 +1,4 @@ +// Flags: --expose-gc import { isWindows, skipIfSQLiteMissing } from '../common/index.mjs'; import tmpdir from '../common/tmpdir.js'; import { join } from 'node:path'; @@ -314,3 +315,43 @@ test('backup has correct name and length', (t) => { t.assert.strictEqual(backup.name, 'backup'); t.assert.strictEqual(backup.length, 2); }); + +test('source database is kept alive while a backup is in flight', async (t) => { + // Regression test: previously, BackupJob stored a raw DatabaseSync* and the + // source could be garbage-collected while the backup was still running, + // leading to a use-after-free when BackupJob::Finalize() dereferenced the + // stale pointer via source_->RemoveBackup(this). + const destDb = nextDb(); + + let database = makeSourceDb(); + // Insert enough rows to ensure the backup takes multiple steps. + const insert = database.prepare('INSERT INTO data (key, value) VALUES (?, ?)'); + for (let i = 3; i <= 500; i++) { + insert.run(i, 'A'.repeat(1024) + i); + } + + const p = backup(database, destDb, { + rate: 1, + progress() {}, + }); + // Drop the last strong JS reference to the source database. With the bug, + // the DatabaseSync could be collected here and the in-flight backup would + // later crash while accessing the freed source. + database = null; + + // Nudge the GC aggressively, but the backup must keep the source alive + // regardless. Without the fix, the source DatabaseSync would be collected + // and BackupJob::Finalize() would crash the process. + for (let i = 0; i < 5; i++) { + global.gc(); + await new Promise((resolve) => setImmediate(resolve)); + } + + const totalPages = await p; + t.assert.ok(totalPages > 0); + + const backupDb = new DatabaseSync(destDb); + t.after(() => { backupDb.close(); }); + const rows = backupDb.prepare('SELECT COUNT(*) AS n FROM data').get(); + t.assert.strictEqual(rows.n, 500); +}); From 5b551a6e1c0679d77e3629f080e38bcb9aa0ff5c Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Thu, 7 May 2026 16:44:56 +0200 Subject: [PATCH 05/60] module: fix sync hook short-circuit in require() in imported CJS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - For imported CJS, if it's not customized by asynchronous hooks, make sure it won't use the quirky re-invented require in all cases. - When the imported CJS module is customized by synchronous hooks, in the synthetic module evalutation step, avoid calling the respective default step again. - Make the branching of loadCJSModuleWithModuleLoad() and loadCJSModuleWithSpecialRequire() more explicit, and fold the tentative fs read in the 'commonjs' translator into the share createCJSModuleWrap() helper instead of checking it twice in the same path. Signed-off-by: Joyee Cheung PR-URL: https://github.com/nodejs/node/pull/62920 Fixes: https://github.com/nodejs/node/issues/63060 Reviewed-By: Paolo Insogna Reviewed-By: Matteo Collina Reviewed-By: Gürgün Dayıoğlu --- lib/internal/modules/cjs/loader.js | 35 +++-- lib/internal/modules/esm/load.js | 6 - lib/internal/modules/esm/loader.js | 133 +++++++++++++----- lib/internal/modules/esm/translators.js | 113 ++++++++++----- lib/internal/test_runner/mock/mock.js | 26 +++- ...ule-hooks-load-import-cjs-custom-source.js | 41 ++++++ 6 files changed, 265 insertions(+), 89 deletions(-) create mode 100644 test/module-hooks/test-module-hooks-load-import-cjs-custom-source.js diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index ad6bd33bad0edc..11a71966981335 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1095,20 +1095,26 @@ function defaultResolveImplForCJSLoading(specifier, parent, isMain, options) { return wrapResolveFilename(specifier, parent, isMain, options); } +/** + * @typedef {{ + * resolved?: {url?: string, format?: string, filename: string}, + * shouldSkipModuleHooks?: boolean, + * source?: string|ArrayBufferView|ArrayBuffer, + * requireResolveOptions?: ResolveFilenameOptions, + * }} CJSModuleLoadInternalOptions + */ + /** * Resolve a module request for CommonJS, invoking hooks from module.registerHooks() * if necessary. * @param {string} specifier * @param {Module|undefined} parent * @param {boolean} isMain - * @param {object} internalResolveOptions - * @param {boolean} internalResolveOptions.shouldSkipModuleHooks Whether to skip module hooks. - * @param {ResolveFilenameOptions} internalResolveOptions.requireResolveOptions Options from require.resolve(). - * Only used when it comes from require.resolve(). + * @param {CJSModuleLoadInternalOptions} internalOptions * @returns {{url?: string, format?: string, parentURL?: string, filename: string}} */ -function resolveForCJSWithHooks(specifier, parent, isMain, internalResolveOptions) { - const { requireResolveOptions, shouldSkipModuleHooks } = internalResolveOptions; +function resolveForCJSWithHooks(specifier, parent, isMain, internalOptions) { + const { requireResolveOptions, shouldSkipModuleHooks } = internalOptions; const defaultResolveImpl = requireResolveOptions ? wrapResolveFilename : defaultResolveImplForCJSLoading; // Fast path: no hooks, just return simple results. @@ -1255,10 +1261,10 @@ function loadBuiltinWithHooks(id, url, format) { * @param {string} request Specifier of module to load via `require` * @param {Module} parent Absolute path of the module importing the child * @param {boolean} isMain Whether the module is the main entry point - * @param {object|undefined} internalResolveOptions Additional options for loading the module + * @param {CJSModuleLoadInternalOptions|undefined} internalOptions Additional options for loading the module * @returns {object} */ -Module._load = function(request, parent, isMain, internalResolveOptions = kEmptyObject) { +Module._load = function(request, parent, isMain, internalOptions = kEmptyObject) { let relResolveCacheIdentifier; if (parent) { debug('Module._load REQUEST %s parent: %s', request, parent.id); @@ -1282,7 +1288,10 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty } } - const resolveResult = resolveForCJSWithHooks(request, parent, isMain, internalResolveOptions); + // If the module has been resolved by a short-circuiting synchronous resolve hook, + // avoid running the default resolution from disk again. + const resolveResult = internalOptions.resolved ?? + resolveForCJSWithHooks(request, parent, isMain, internalOptions); let { format } = resolveResult; const { url, filename } = resolveResult; @@ -1370,6 +1379,14 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty module[kLastModuleParent] = parent; } + // The module source was provided by a short-circuiting synchronous hook, + // assign them into the module to avoid triggering the default load step again. + if (internalOptions.source !== undefined) { + module[kModuleSource] ??= internalOptions.source; + module[kURL] ??= url; + module[kFormat] ??= format; + } + if (parent !== undefined) { relativeResolveCache[relResolveCacheIdentifier] = filename; } diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js index e8658716f881a9..94879761553e02 100644 --- a/lib/internal/modules/esm/load.js +++ b/lib/internal/modules/esm/load.js @@ -145,7 +145,6 @@ function defaultLoadSync(url, context = kEmptyObject) { throwIfUnsupportedURLScheme(urlInstance, false); - let shouldBeReloadedByCJSLoader = false; if (urlInstance.protocol === 'node:') { source = null; format ??= 'builtin'; @@ -160,10 +159,6 @@ function defaultLoadSync(url, context = kEmptyObject) { // Now that we have the source for the module, run `defaultGetFormat` to detect its format. format ??= defaultGetFormat(urlInstance, context); - - // For backward compatibility reasons, we need to let go through Module._load - // again. - shouldBeReloadedByCJSLoader = (format === 'commonjs'); } validateAttributes(url, format, importAttributes); @@ -172,7 +167,6 @@ function defaultLoadSync(url, context = kEmptyObject) { format, responseURL, source, - shouldBeReloadedByCJSLoader, }; } diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index cf147b964b47a3..2aa1dbd499c8fb 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -120,7 +120,19 @@ const { defaultLoadSync, throwUnknownModuleFormat } = require('internal/modules/ */ /** - * @typedef {{ format: ModuleFormat, source: ModuleSource, translatorKey: string }} TranslateContext + * @typedef {{format: string, url: string, isResolvedBySyncHooks: boolean}} ResolveResult + */ + +/** + * @typedef {{ + * url: string, + * format: ModuleFormat, + * source: ModuleSource, + * responseURL?: string, + * translatorKey: string, + * isResolvedBySyncHooks: boolean, + * isSourceLoadedSynchronously: boolean, + * }} TranslateContext */ /** @@ -382,19 +394,25 @@ class ModuleLoader { /** * Load a module and translate it into a ModuleWrap for require(esm). * This is run synchronously, and the translator always return a ModuleWrap synchronously. - * @param {string} url URL of the module to be translated. + * @param {ResolveResult} resolveResult Result from the resolve step. * @param {object} loadContext See {@link load} * @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point. * @param {ModuleRequest} request Module request. * @returns {ModuleWrap} */ - loadAndTranslateForImportInRequiredESM(url, loadContext, parentURL, request) { + loadAndTranslateForImportInRequiredESM(resolveResult, loadContext, parentURL, request) { + const { url } = resolveResult; const loadResult = this.#loadSync(url, loadContext); // Use the synchronous commonjs translator which can deal with cycles. const formatFromLoad = loadResult.format; const translatorKey = (formatFromLoad === 'commonjs' || formatFromLoad === 'commonjs-typescript') ? 'commonjs-sync' : formatFromLoad; - const translateContext = { ...loadResult, translatorKey, __proto__: null }; + const translateContext = { + ...resolveResult, + ...loadResult, + translatorKey, + __proto__: null, + }; const wrap = this.#translate(url, translateContext, parentURL); assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`); @@ -443,12 +461,13 @@ class ModuleLoader { /** * Load a module and translate it into a ModuleWrap for require() in imported CJS. * This is run synchronously, and the translator always return a ModuleWrap synchronously. - * @param {string} url URL of the module to be translated. + * @param {ResolveResult} resolveResult Result from the resolve step. * @param {object} loadContext See {@link load} * @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point. * @returns {ModuleWrap} */ - loadAndTranslateForRequireInImportedCJS(url, loadContext, parentURL) { + loadAndTranslateForRequireInImportedCJS(resolveResult, loadContext, parentURL) { + const { url } = resolveResult; const loadResult = this.#loadSync(url, loadContext); const formatFromLoad = loadResult.format; @@ -470,7 +489,12 @@ class ModuleLoader { translatorKey = 'require-commonjs-typescript'; } - const translateContext = { ...loadResult, translatorKey, __proto__: null }; + const translateContext = { + ...resolveResult, + ...loadResult, + translatorKey, + __proto__: null, + }; const wrap = this.#translate(url, translateContext, parentURL); assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`); return wrap; @@ -479,15 +503,21 @@ class ModuleLoader { /** * Load a module and translate it into a ModuleWrap for ordinary imported ESM. * This may be run asynchronously if there are asynchronous module loader hooks registered. - * @param {string} url URL of the module to be translated. + * @param {ResolveResult} resolveResult Result from the resolve step. * @param {object} loadContext See {@link load} * @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point. * @returns {Promise|ModuleWrap} */ - loadAndTranslate(url, loadContext, parentURL) { + loadAndTranslate(resolveResult, loadContext, parentURL) { + const { url } = resolveResult; const maybePromise = this.load(url, loadContext); const afterLoad = (loadResult) => { - const translateContext = { ...loadResult, translatorKey: loadResult.format, __proto__: null }; + const translateContext = { + ...resolveResult, + ...loadResult, + translatorKey: loadResult.format, + __proto__: null, + }; return this.#translate(url, translateContext, parentURL); }; if (isPromise(maybePromise)) { @@ -503,7 +533,7 @@ class ModuleLoader { * the module should be linked by the time this returns. Otherwise it may still have * pending module requests. * @param {string} parentURL See {@link getOrCreateModuleJob} - * @param {{format: string, url: string}} resolveResult + * @param {ResolveResult} resolveResult * @param {ModuleRequest} request Module request. * @param {ModuleRequestType} requestType Type of the module request. * @returns {ModuleJobBase} The (possibly pending) module job @@ -542,11 +572,11 @@ class ModuleLoader { let moduleOrModulePromise; if (requestType === kRequireInImportedCJS) { - moduleOrModulePromise = this.loadAndTranslateForRequireInImportedCJS(url, context, parentURL); + moduleOrModulePromise = this.loadAndTranslateForRequireInImportedCJS(resolveResult, context, parentURL); } else if (requestType === kImportInRequiredESM) { - moduleOrModulePromise = this.loadAndTranslateForImportInRequiredESM(url, context, parentURL, request); + moduleOrModulePromise = this.loadAndTranslateForImportInRequiredESM(resolveResult, context, parentURL, request); } else { - moduleOrModulePromise = this.loadAndTranslate(url, context, parentURL); + moduleOrModulePromise = this.loadAndTranslate(resolveResult, context, parentURL); } if (requestType === kImportInRequiredESM || requestType === kRequireInImportedCJS || @@ -660,7 +690,7 @@ class ModuleLoader { * @param {string} [parentURL] The URL of the module where the module request is initiated. * It's undefined if it's from the root module. * @param {ModuleRequest} request Module request. - * @returns {Promise<{format: string, url: string}>|{format: string, url: string}} + * @returns {Promise|ResolveResult} */ #resolve(parentURL, request) { if (this.isForAsyncLoaderHookWorker) { @@ -696,15 +726,18 @@ class ModuleLoader { /** * This is the default resolve step for module.registerHooks(), which incorporates asynchronous hooks * from module.register() which are run in a blocking fashion for it to be synchronous. + * @param {{isResolvedByDefaultResolve: boolean}} out Output object to track whether the default resolve was used + * without polluting the user-visible resolve result. * @param {string|URL} specifier See {@link resolveSync}. * @param {{ parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context * See {@link resolveSync}. * @returns {{ format: string, url: string }} */ - #resolveAndMaybeBlockOnLoaderThread(specifier, context) { + #resolveAndMaybeBlockOnLoaderThread(out, specifier, context) { if (this.#asyncLoaderHooks?.resolveSync) { return this.#asyncLoaderHooks.resolveSync(specifier, context.parentURL, context.importAttributes); } + out.isResolvedByDefaultResolve = true; return this.#cachedDefaultResolve(specifier, context); } @@ -719,31 +752,45 @@ class ModuleLoader { * @param {boolean} [shouldSkipSyncHooks] Whether to skip the synchronous hooks registered by module.registerHooks(). * This is used to maintain compatibility for the re-invented require.resolve (in imported CJS customized * by module.register()`) which invokes the CJS resolution separately from the hook chain. - * @returns {{ format: string, url: string }} + * @returns {ResolveResult} */ resolveSync(parentURL, request, shouldSkipSyncHooks = false) { const specifier = `${request.specifier}`; const importAttributes = request.attributes ?? kEmptyObject; + // Use an output parameter to track the state and avoid polluting the user-visible resolve results. + const out = { isResolvedByDefaultResolve: false, __proto__: null }; + let result; + let isResolvedBySyncHooks = false; if (!shouldSkipSyncHooks && syncResolveHooks.length) { // Has module.registerHooks() hooks, chain the asynchronous hooks in the default step. - return resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions, - this.#resolveAndMaybeBlockOnLoaderThread.bind(this)); + result = resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions, + this.#resolveAndMaybeBlockOnLoaderThread.bind(this, out)); + // If the default step ran, sync hooks did not short-circuit the resolution. + isResolvedBySyncHooks = !out.isResolvedByDefaultResolve; + } else { + const context = { + ...request, + conditions: this.#defaultConditions, + parentURL, + importAttributes, + __proto__: null, + }; + result = this.#resolveAndMaybeBlockOnLoaderThread(out, specifier, context); } - const context = { - ...request, - conditions: this.#defaultConditions, - parentURL, - importAttributes, - __proto__: null, - }; - return this.#resolveAndMaybeBlockOnLoaderThread(specifier, context); + result.isResolvedBySyncHooks = isResolvedBySyncHooks; + return result; } /** * Provide source that is understood by one of Node's translators. Handles customization hooks, * if any. - * @typedef { {format: ModuleFormat, source: ModuleSource }} LoadResult + * @typedef {{ + * format: ModuleFormat, + * source: ModuleSource, + * responseURL?: string, + * isSourceLoadedSynchronously: boolean, + * }} LoadResult * @param {string} url The URL of the module to be loaded. * @param {object} context Metadata about the module * @returns {Promise | LoadResult}} @@ -759,14 +806,19 @@ class ModuleLoader { /** * This is the default load step for module.registerHooks(), which incorporates asynchronous hooks * from module.register() which are run in a blocking fashion for it to be synchronous. + * @param {{isSourceLoadedSynchronously: boolean}} out + * Output object to track whether the source was loaded synchronously without polluting + * the user-visible load result. * @param {string} url See {@link load} * @param {object} context See {@link load} * @returns {{ format: ModuleFormat, source: ModuleSource }} */ - #loadAndMaybeBlockOnLoaderThread(url, context) { + #loadAndMaybeBlockOnLoaderThread(out, url, context) { if (this.#asyncLoaderHooks?.loadSync) { + out.isSourceLoadedSynchronously = false; return this.#asyncLoaderHooks.loadSync(url, context); } + out.isSourceLoadedSynchronously = true; return defaultLoadSync(url, context); } @@ -777,17 +829,32 @@ class ModuleLoader { * This is here to support `require()` in imported CJS and `module.registerHooks()` hooks. * @param {string} url See {@link load} * @param {object} [context] See {@link load} - * @returns {{ format: ModuleFormat, source: ModuleSource }} + * @returns {LoadResult} */ #loadSync(url, context) { + // Use an output parameter to track the state and avoid polluting the user-visible resolve results. + const out = { + isSourceLoadedSynchronously: true, + __proto__: null, + }; + let result; if (syncLoadHooks.length) { // Has module.registerHooks() hooks, chain the asynchronous hooks in the default step. // TODO(joyeecheung): construct the ModuleLoadContext in the loaders directly instead // of converting them from plain objects in the hooks. - return loadWithSyncHooks(url, context.format, context.importAttributes, this.#defaultConditions, - this.#loadAndMaybeBlockOnLoaderThread.bind(this), validateLoadSloppy); + result = loadWithSyncHooks( + url, + context.format, + context.importAttributes, + this.#defaultConditions, + this.#loadAndMaybeBlockOnLoaderThread.bind(this, out), + validateLoadSloppy, + ); + } else { + result = this.#loadAndMaybeBlockOnLoaderThread(out, url, context); } - return this.#loadAndMaybeBlockOnLoaderThread(url, context); + result.isSourceLoadedSynchronously = out.isSourceLoadedSynchronously; + return result; } validateLoadResult(url, format) { diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index a5a0b947e78283..f4234247aa9bce 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -23,7 +23,6 @@ const { const { BuiltinModule } = require('internal/bootstrap/realm'); const assert = require('internal/assert'); -const fs = require('fs'); const { dirname, extname } = require('path'); const { assertBufferSource, @@ -96,6 +95,8 @@ const kShouldSkipModuleHooks = { __proto__: null, shouldSkipModuleHooks: true }; const kShouldNotSkipModuleHooks = { __proto__: null, shouldSkipModuleHooks: false }; /** + * This may be eventually removed when module.register() reaches end-of-life. + * * Loads a CommonJS module via the ESM Loader sync CommonJS translator. * This translator creates its own version of the `require` function passed into CommonJS modules. * Any monkey patches applied to the CommonJS Loader will not affect this module. @@ -105,8 +106,9 @@ const kShouldNotSkipModuleHooks = { __proto__: null, shouldSkipModuleHooks: fals * @param {string} url - The URL of the module. * @param {string} filename - The filename of the module. * @param {boolean} isMain - Whether the module is the entrypoint + * @param {TranslateContext} translateContext Context for the translator */ -function loadCJSModule(module, source, url, filename, isMain) { +function loadCJSModuleWithSpecialRequire(module, source, url, filename, isMain, translateContext) { // Use the full URL as the V8 resource name so that any search params // (e.g. ?node-test-mock) are preserved in coverage reports. const compileResult = compileFunctionForCJSLoader(source, url, false /* is_sea_main */, false); @@ -193,21 +195,24 @@ const cjsCache = new SafeMap(); /** * Creates a ModuleWrap object for a CommonJS module. * @param {string} url - The URL of the module. - * @param {{ format: ModuleFormat, source: ModuleSource }} translateContext Context for the translator + * @param {import('./loader').TranslateContext} translateContext Context for the translator * @param {string|undefined} parentURL URL of the module initiating the module loading for the first time. * Undefined if it's the entry point. - * @param {typeof loadCJSModule} [loadCJS] - The function to load the CommonJS module. * @returns {ModuleWrap} The ModuleWrap object for the CommonJS module. */ -function createCJSModuleWrap(url, translateContext, parentURL, loadCJS = loadCJSModule) { +function createCJSModuleWrap(url, translateContext, parentURL) { debug(`Translating CJSModule ${url}`, translateContext); const { format: sourceFormat } = translateContext; let { source } = translateContext; const isMain = (parentURL === undefined); const filename = urlToFilename(url); - // In case the source was not provided by the `load` step, we need fetch it now. - source = stringify(source ?? getSourceSync(new URL(url)).source); + try { + // In case the source was not provided by the `load` step, we need fetch it now. + source = stringify(source ?? getSourceSync(new URL(url)).source); + } catch { + // Continue regardless of error. + } const { exportNames, module } = cjsPreparseModuleExports(filename, source, sourceFormat); cjsCache.set(url, module); @@ -228,7 +233,19 @@ function createCJSModuleWrap(url, translateContext, parentURL, loadCJS = loadCJS debug(`Loading CJSModule ${url}`); if (!module.loaded) { - loadCJS(module, source, url, filename, !!isMain); + // For backward-compatibility, it's possible for async hooks to return a nullish value for + // CJS source associated with a `file:` URL - that usually means the source is not + // customized (is loaded by default load) or the hook author wants it to be reloaded + // through CJS routine. In this case, the source is obtained by calling the + // Module._load(). + if (translateContext.translatorKey === 'commonjs-sync' || + translateContext.isSourceLoadedSynchronously || + translateContext.source == null) { + loadCJSModuleWithModuleLoad(module, source, url, filename, !!isMain, translateContext); + } else { // CommonJS with source customized by async hooks + // This may be eventually removed when module.register() reaches end-of-life. + loadCJSModuleWithSpecialRequire(module, source, url, filename, !!isMain, translateContext); + } } let exports; @@ -302,55 +319,73 @@ function createCJSNoSourceModuleWrap(url, parentURL) { } translators.set('commonjs-sync', function requireCommonJS(url, translateContext, parentURL) { - return createCJSModuleWrap(url, translateContext, parentURL, loadCJSModuleWithModuleLoad); + return createCJSModuleWrap(url, translateContext, parentURL); }); -// Handle CommonJS modules referenced by `require` calls. -// This translator function must be sync, as `require` is sync. +// Handle CommonJS modules referenced by `require` calls using re-invented require. +// This path is only used by require() from imported CJS customized by the *async* +// loader hooks. translators.set('require-commonjs', (url, translateContext, parentURL) => { return createCJSModuleWrap(url, translateContext, parentURL); }); -// Handle CommonJS modules referenced by `require` calls. -// This translator function must be sync, as `require` is sync. +// Handle TypeScript CommonJS modules referenced by `require` calls using re-invented require. +// This path is only used by require() from imported CJS customized by the *async* +// loader hooks. translators.set('require-commonjs-typescript', (url, translateContext, parentURL) => { translateContext.source = stripTypeScriptModuleTypes(stringify(translateContext.source), url); return createCJSModuleWrap(url, translateContext, parentURL); }); // This goes through Module._load to accommodate monkey-patchers. -function loadCJSModuleWithModuleLoad(module, source, url, filename, isMain) { +/** + * Loads a CommonJS module through Module._load to accommodate monkey-patchers. + * If the module was resolved by synchronous hooks (i.e. not by the default resolver), + * passes the pre-resolved information and source to Module._load to avoid + * re-resolving and re-loading. + * @param {import('internal/modules/cjs/loader').Module} module - The module to load. + * @param {string} source - The source code of the module. + * @param {string} url - The URL of the module. + * @param {string} filename - The filename of the module. + * @param {boolean} isMain - Whether the module is the entrypoint + * @param {import('./loader').TranslateContext} translateContext Context for the translator + */ +function loadCJSModuleWithModuleLoad(module, source, url, filename, isMain, translateContext) { assert(module === CJSModule._cache[filename]); - // If it gets here in the translators, the hooks must have already been invoked - // in the loader. Skip them in the synthetic module evaluation step. - wrapModuleLoad(filename, undefined, isMain, kShouldSkipModuleHooks); + debug(`loadCJSModuleWithModuleLoad ${url}`); + let exports; + if (translateContext.isResolvedBySyncHooks) { + exports = wrapModuleLoad(filename, undefined, isMain, { + __proto__: null, + resolved: { + __proto__: null, + filename, + format: translateContext.format, + url, + }, + shouldSkipModuleHooks: true, + source, + }); + } else { + // If it gets here in the translators, the hooks must have already been invoked + // in the loader. Skip them in the synthetic module evaluation step. + exports = wrapModuleLoad(filename, undefined, isMain, kShouldSkipModuleHooks); + } + + // Patched Module._load implementations may return exports without updating the + // ESM-created cache entry. Mirror the returned value into the translator-owned + // module so the synthetic module namespace observes the loaded exports. + if (!module.loaded) { + module.exports = exports; + module.loaded = true; + } + module[kModuleExport] = exports; } // Handle CommonJS modules referenced by `import` statements or expressions, // or as the initial entry point when the ESM loader handles a CommonJS entry. translators.set('commonjs', function commonjsStrategy(url, translateContext, parentURL) { - // For backward-compatibility, it's possible to return a nullish value for - // CJS source associated with a `file:` URL - that usually means the source is not - // customized (is loaded by default load) or the hook author wants it to be reloaded - // through CJS routine. In this case, the source is obtained by calling the - // monkey-patchable CJS loader. - // TODO(joyeecheung): just use wrapModuleLoad and let the CJS loader - // invoke the off-thread hooks. Use a special parent to avoid invoking in-thread - // hooks twice. - const shouldReloadByCJSLoader = (translateContext.shouldBeReloadedByCJSLoader || translateContext.source == null); - const cjsLoader = shouldReloadByCJSLoader ? loadCJSModuleWithModuleLoad : loadCJSModule; - - try { - // We still need to read the FS to detect the exports. - // If you are reading this code to figure out how to patch Node.js module loading - // behavior - DO NOT depend on the patchability in new code: Node.js - // internals may stop going through the JavaScript fs module entirely. - // Prefer module.registerHooks() or other more formal fs hooks released in the future. - translateContext.source ??= fs.readFileSync(new URL(url), 'utf8'); - } catch { - // Continue regardless of error. - } - return createCJSModuleWrap(url, translateContext, parentURL, cjsLoader); + return createCJSModuleWrap(url, translateContext, parentURL); }); /** diff --git a/lib/internal/test_runner/mock/mock.js b/lib/internal/test_runner/mock/mock.js index fb1ed322b414fc..d15ace222b2132 100644 --- a/lib/internal/test_runner/mock/mock.js +++ b/lib/internal/test_runner/mock/mock.js @@ -51,8 +51,15 @@ const { validateOneOf, } = require('internal/validators'); const { MockTimers } = require('internal/test_runner/mock/mock_timers'); -const { Module } = require('internal/modules/cjs/loader'); -const { _load, _nodeModulePaths, _resolveFilename, isBuiltin } = Module; +const { + Module, +} = require('internal/modules/cjs/loader'); +const { + _load, + _nodeModulePaths, + _resolveFilename, + isBuiltin, +} = Module; function kDefaultFunction() {} const enableModuleMocking = getOptionValue('--experimental-test-module-mocks'); const kSupportedFormats = [ @@ -905,6 +912,21 @@ function setupSharedModuleState() { } function cjsMockModuleLoad(request, parent, isMain) { + // Imported mocked URLs may re-enter Module._load with the mock query attached. + // Strip it to pass into methods that expect a normal request. + // TODO(joyeecheung): it might be better to strip the search params from the filename in + // the translator but that might have a bigger blast radius as other mocker might have also + // come to rely on this to create multiple cache identities for the same module. + try { + const parsedRequest = URLParse(request); + if (parsedRequest?.searchParams.has(kMockSearchParam)) { + parsedRequest.searchParams.delete(kMockSearchParam); + request = parsedRequest.href; + } + } catch { + // Not a valid URL, treat as a normal request. + } + let resolved; if (isBuiltin(request)) { diff --git a/test/module-hooks/test-module-hooks-load-import-cjs-custom-source.js b/test/module-hooks/test-module-hooks-load-import-cjs-custom-source.js new file mode 100644 index 00000000000000..723f5158cebc9f --- /dev/null +++ b/test/module-hooks/test-module-hooks-load-import-cjs-custom-source.js @@ -0,0 +1,41 @@ +'use strict'; +// Test that imported CJS loaded from sync hooks are handled correctly and +// won't invoke the CJS loading steps again (in which case it would throw +// because the source is not on disk). + +const common = require('../common'); +const assert = require('assert'); +const { registerHooks } = require('module'); + +const virtualModules = new Map([ + ['virtual:shared', 'module.exports.greet = (name) => "hello " + name;'], + ['virtual:entry', 'const { greet } = require("virtual:shared");\nmodule.exports = greet("world");'], +]); + +const hook = registerHooks({ + resolve: common.mustCall((specifier, context, nextResolve) => { + if (virtualModules.has(specifier)) { + return { + url: specifier, + format: 'commonjs', + shortCircuit: true, + }; + } + return nextResolve(specifier, context); + }, 2), + load: common.mustCall((url, context, nextLoad) => { + if (virtualModules.has(url)) { + return { + format: 'commonjs', + source: virtualModules.get(url), + shortCircuit: true, + }; + } + return nextLoad(url, context); + }, 2), +}); + +import('virtual:entry').then(common.mustCall((ns) => { + assert.strictEqual(ns.default, 'hello world'); + hook.deregister(); +})); From 4a031498dacc5b257542e3180cafe22d68c1e0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 8 May 2026 14:29:34 +0100 Subject: [PATCH 06/60] test: use ERM to destroy sqlite database handles after tests Signed-off-by: Renegade334 PR-URL: https://github.com/nodejs/node/pull/63076 Refs: https://github.com/nodejs/node/issues/63052 Reviewed-By: Chemi Atlow Reviewed-By: Luigi Pinca Reviewed-By: Edy Silva --- .../test-sqlite-database-sync-dispose.js | 33 ------- test/parallel/test-sqlite-database-sync.js | 99 +++++++++---------- 2 files changed, 46 insertions(+), 86 deletions(-) delete mode 100644 test/parallel/test-sqlite-database-sync-dispose.js diff --git a/test/parallel/test-sqlite-database-sync-dispose.js b/test/parallel/test-sqlite-database-sync-dispose.js deleted file mode 100644 index 67a1ab6757b848..00000000000000 --- a/test/parallel/test-sqlite-database-sync-dispose.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; -const { skipIfSQLiteMissing } = require('../common'); -skipIfSQLiteMissing(); -const tmpdir = require('../common/tmpdir'); -const assert = require('node:assert'); -const { join } = require('node:path'); -const { DatabaseSync } = require('node:sqlite'); -const { suite, test } = require('node:test'); -let cnt = 0; - -tmpdir.refresh(); - -function nextDb() { - return join(tmpdir.path, `database-${cnt++}.db`); -} - -suite('DatabaseSync.prototype[Symbol.dispose]()', () => { - test('closes an open database', () => { - const db = new DatabaseSync(nextDb()); - db[Symbol.dispose](); - assert.throws(() => { - db.close(); - }, /database is not open/); - }); - - test('supports databases that are not open', () => { - const db = new DatabaseSync(nextDb(), { open: false }); - db[Symbol.dispose](); - assert.throws(() => { - db.close(); - }, /database is not open/); - }); -}); diff --git a/test/parallel/test-sqlite-database-sync.js b/test/parallel/test-sqlite-database-sync.js index d778f839098737..ac3a3c66d6469a 100644 --- a/test/parallel/test-sqlite-database-sync.js +++ b/test/parallel/test-sqlite-database-sync.js @@ -89,19 +89,18 @@ suite('DatabaseSync() constructor', () => { test('is not read-only by default', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath); + using db = new DatabaseSync(dbPath); db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)'); }); test('is read-only if readOnly is set', (t) => { const dbPath = nextDb(); { - const db = new DatabaseSync(dbPath); + using db = new DatabaseSync(dbPath); db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)'); - db.close(); } { - const db = new DatabaseSync(dbPath, { readOnly: true }); + using db = new DatabaseSync(dbPath, { readOnly: true }); t.assert.throws(() => { db.exec('CREATE TABLE bar (id INTEGER PRIMARY KEY)'); }, { @@ -122,12 +121,11 @@ suite('DatabaseSync() constructor', () => { test('enables foreign key constraints by default', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath); + using db = new DatabaseSync(dbPath); db.exec(` CREATE TABLE foo (id INTEGER PRIMARY KEY); CREATE TABLE bar (foo_id INTEGER REFERENCES foo(id)); `); - t.after(() => { db.close(); }); t.assert.throws(() => { db.exec('INSERT INTO bar (foo_id) VALUES (1)'); }, { @@ -138,12 +136,11 @@ suite('DatabaseSync() constructor', () => { test('allows disabling foreign key constraints', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath, { enableForeignKeyConstraints: false }); + using db = new DatabaseSync(dbPath, { enableForeignKeyConstraints: false }); db.exec(` CREATE TABLE foo (id INTEGER PRIMARY KEY); CREATE TABLE bar (foo_id INTEGER REFERENCES foo(id)); `); - t.after(() => { db.close(); }); db.exec('INSERT INTO bar (foo_id) VALUES (1)'); }); @@ -158,8 +155,7 @@ suite('DatabaseSync() constructor', () => { test('disables double-quoted string literals by default', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath); - t.after(() => { db.close(); }); + using db = new DatabaseSync(dbPath); t.assert.throws(() => { db.exec('SELECT "foo";'); }, { @@ -170,8 +166,7 @@ suite('DatabaseSync() constructor', () => { test('allows enabling double-quoted string literals', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath, { enableDoubleQuotedStringLiterals: true }); - t.after(() => { db.close(); }); + using db = new DatabaseSync(dbPath, { enableDoubleQuotedStringLiterals: true }); db.exec('SELECT "foo";'); }); @@ -186,8 +181,7 @@ suite('DatabaseSync() constructor', () => { test('allows reading big integers', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath, { readBigInts: true }); - t.after(() => { db.close(); }); + using db = new DatabaseSync(dbPath, { readBigInts: true }); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT; @@ -216,8 +210,7 @@ suite('DatabaseSync() constructor', () => { test('allows returning arrays', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath, { returnArrays: true }); - t.after(() => { db.close(); }); + using db = new DatabaseSync(dbPath, { returnArrays: true }); const setup = db.exec(` CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; INSERT INTO data (key, val) VALUES (1, 'one'); @@ -240,8 +233,7 @@ suite('DatabaseSync() constructor', () => { test('throws if bare named parameters are used when option is false', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath, { allowBareNamedParameters: false }); - t.after(() => { db.close(); }); + using db = new DatabaseSync(dbPath, { allowBareNamedParameters: false }); const setup = db.exec( 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' ); @@ -267,8 +259,7 @@ suite('DatabaseSync() constructor', () => { test('allows unknown named parameters', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath, { allowUnknownNamedParameters: true }); - t.after(() => { db.close(); }); + using db = new DatabaseSync(dbPath, { allowUnknownNamedParameters: true }); const setup = db.exec( 'CREATE TABLE data(key INTEGER, val INTEGER) STRICT;' ); @@ -284,8 +275,7 @@ suite('DatabaseSync() constructor', () => { test('has sqlite-type symbol property', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath); - t.after(() => { db.close(); }); + using db = new DatabaseSync(dbPath); const sqliteTypeSymbol = Symbol.for('sqlite-type'); t.assert.strictEqual(db[sqliteTypeSymbol], 'node:sqlite'); @@ -295,8 +285,7 @@ suite('DatabaseSync() constructor', () => { suite('DatabaseSync.prototype.open()', () => { test('opens a database connection', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath, { open: false }); - t.after(() => { db.close(); }); + using db = new DatabaseSync(dbPath, { open: false }); t.assert.strictEqual(db.isOpen, false); t.assert.strictEqual(existsSync(dbPath), false); @@ -306,8 +295,7 @@ suite('DatabaseSync.prototype.open()', () => { }); test('throws if database is already open', (t) => { - const db = new DatabaseSync(nextDb(), { open: false }); - t.after(() => { db.close(); }); + using db = new DatabaseSync(nextDb(), { open: false }); t.assert.strictEqual(db.isOpen, false); db.open(); @@ -324,7 +312,7 @@ suite('DatabaseSync.prototype.open()', () => { suite('DatabaseSync.prototype.close()', () => { test('closes an open database connection', (t) => { - const db = new DatabaseSync(nextDb()); + using db = new DatabaseSync(nextDb()); t.assert.strictEqual(db.isOpen, true); t.assert.strictEqual(db.close(), undefined); @@ -332,7 +320,7 @@ suite('DatabaseSync.prototype.close()', () => { }); test('throws if database is not open', (t) => { - const db = new DatabaseSync(nextDb(), { open: false }); + using db = new DatabaseSync(nextDb(), { open: false }); t.assert.strictEqual(db.isOpen, false); t.assert.throws(() => { @@ -347,14 +335,13 @@ suite('DatabaseSync.prototype.close()', () => { suite('DatabaseSync.prototype.prepare()', () => { test('returns a prepared statement', (t) => { - const db = new DatabaseSync(nextDb()); - t.after(() => { db.close(); }); + using db = new DatabaseSync(nextDb()); const stmt = db.prepare('CREATE TABLE webstorage(key TEXT)'); t.assert.ok(stmt instanceof StatementSync); }); test('throws if database is not open', (t) => { - const db = new DatabaseSync(nextDb(), { open: false }); + using db = new DatabaseSync(nextDb(), { open: false }); t.assert.throws(() => { db.prepare(); @@ -365,8 +352,7 @@ suite('DatabaseSync.prototype.prepare()', () => { }); test('throws if sql is not a string', (t) => { - const db = new DatabaseSync(nextDb()); - t.after(() => { db.close(); }); + using db = new DatabaseSync(nextDb()); t.assert.throws(() => { db.prepare(); @@ -379,8 +365,7 @@ suite('DatabaseSync.prototype.prepare()', () => { suite('DatabaseSync.prototype.exec()', () => { test('executes SQL', (t) => { - const db = new DatabaseSync(nextDb()); - t.after(() => { db.close(); }); + using db = new DatabaseSync(nextDb()); const result = db.exec(` CREATE TABLE data( key INTEGER PRIMARY KEY, @@ -398,8 +383,7 @@ suite('DatabaseSync.prototype.exec()', () => { }); test('reports errors from SQLite', (t) => { - const db = new DatabaseSync(nextDb()); - t.after(() => { db.close(); }); + using db = new DatabaseSync(nextDb()); t.assert.throws(() => { db.exec('CREATE TABLEEEE'); @@ -419,7 +403,7 @@ suite('DatabaseSync.prototype.exec()', () => { }); test('throws if database is not open', (t) => { - const db = new DatabaseSync(nextDb(), { open: false }); + using db = new DatabaseSync(nextDb(), { open: false }); t.assert.throws(() => { db.exec(); @@ -430,8 +414,7 @@ suite('DatabaseSync.prototype.exec()', () => { }); test('throws if sql is not a string', (t) => { - const db = new DatabaseSync(nextDb()); - t.after(() => { db.close(); }); + using db = new DatabaseSync(nextDb()); t.assert.throws(() => { db.exec(); @@ -444,7 +427,7 @@ suite('DatabaseSync.prototype.exec()', () => { suite('DatabaseSync.prototype.isTransaction', () => { test('correctly detects a committed transaction', (t) => { - const db = new DatabaseSync(':memory:'); + using db = new DatabaseSync(':memory:'); t.assert.strictEqual(db.isTransaction, false); db.exec('BEGIN'); @@ -456,7 +439,7 @@ suite('DatabaseSync.prototype.isTransaction', () => { }); test('correctly detects a rolled back transaction', (t) => { - const db = new DatabaseSync(':memory:'); + using db = new DatabaseSync(':memory:'); t.assert.strictEqual(db.isTransaction, false); db.exec('BEGIN'); @@ -468,7 +451,7 @@ suite('DatabaseSync.prototype.isTransaction', () => { }); test('throws if database is not open', (t) => { - const db = new DatabaseSync(nextDb(), { open: false }); + using db = new DatabaseSync(nextDb(), { open: false }); t.assert.throws(() => { return db.isTransaction; @@ -481,7 +464,7 @@ suite('DatabaseSync.prototype.isTransaction', () => { suite('DatabaseSync.prototype.location()', () => { test('throws if database is not open', (t) => { - const db = new DatabaseSync(nextDb(), { open: false }); + using db = new DatabaseSync(nextDb(), { open: false }); t.assert.throws(() => { db.location(); @@ -492,8 +475,7 @@ suite('DatabaseSync.prototype.location()', () => { }); test('throws if provided dbName is not string', (t) => { - const db = new DatabaseSync(nextDb()); - t.after(() => { db.close(); }); + using db = new DatabaseSync(nextDb()); t.assert.throws(() => { db.location(null); @@ -504,24 +486,20 @@ suite('DatabaseSync.prototype.location()', () => { }); test('returns null when connected to in-memory database', (t) => { - const db = new DatabaseSync(':memory:'); + using db = new DatabaseSync(':memory:'); t.assert.strictEqual(db.location(), null); }); test('returns db path when connected to a persistent database', (t) => { const dbPath = nextDb(); - const db = new DatabaseSync(dbPath); - t.after(() => { db.close(); }); + using db = new DatabaseSync(dbPath); t.assert.strictEqual(db.location(), dbPath); }); test('returns that specific db path when attached', (t) => { const dbPath = nextDb(); const otherPath = nextDb(); - const db = new DatabaseSync(dbPath); - t.after(() => { db.close(); }); - const other = new DatabaseSync(dbPath); - t.after(() => { other.close(); }); + using db = new DatabaseSync(dbPath); // Adding this escape because the test with unusual chars have a single quote which breaks the query const escapedPath = otherPath.replace("'", "''"); @@ -530,3 +508,18 @@ suite('DatabaseSync.prototype.location()', () => { t.assert.strictEqual(db.location('other'), otherPath); }); }); + +suite('DatabaseSync.prototype[Symbol.dispose]', () => { + test('closes an open database', (t) => { + const db = new DatabaseSync(nextDb()); + t.assert.strictEqual(db.isOpen, true); + db[Symbol.dispose](); + t.assert.strictEqual(db.isOpen, false); + }); + + test('does not throw on databases that are not open', (t) => { + const db = new DatabaseSync(nextDb(), { open: false }); + t.assert.strictEqual(db.isOpen, false); + db[Symbol.dispose](); + }); +}); From 742a124af6dff7669cdfb842463a86b4f1c8ab5c Mon Sep 17 00:00:00 2001 From: Edy Silva Date: Fri, 8 May 2026 11:15:57 -0300 Subject: [PATCH 07/60] doc,sqlite: document entryPoint argument for loadExtension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: geeksilva97 PR-URL: https://github.com/nodejs/node/pull/63152 Reviewed-By: Colin Ihrig Reviewed-By: René --- doc/api/sqlite.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index 2bc286e955ec6e..42c3623830a657 100644 --- a/doc/api/sqlite.md +++ b/doc/api/sqlite.md @@ -287,7 +287,7 @@ added: v22.5.0 Closes the database connection. An exception is thrown if the database is not open. This method is a wrapper around [`sqlite3_close_v2()`][]. -### `database.loadExtension(path)` +### `database.loadExtension(path[, entryPoint])` * `path` {string} The path to the shared library to load. +* `entryPoint` {string} The name of the extension's entry-point function. When + omitted, SQLite derives the entry point from the shared library's filename; + pass this argument explicitly when the derived name does not match. Loads a shared library into the database connection. This method is a wrapper around [`sqlite3_load_extension()`][]. It is required to enable the `allowExtension` option when constructing the `DatabaseSync` instance. +```mjs +import { DatabaseSync } from 'node:sqlite'; +const database = new DatabaseSync(':memory:', { allowExtension: true }); + +// Load using the entry point derived from the filename. +database.loadExtension('./decimal.dylib'); + +// Override the entry point when the derived name does not match. +database.loadExtension('./base64.dylib', 'sqlite3_base64_init'); +``` + +```cjs +'use strict'; +const { DatabaseSync } = require('node:sqlite'); +const database = new DatabaseSync(':memory:', { allowExtension: true }); + +// Load using the entry point derived from the filename. +database.loadExtension('./decimal.dylib'); + +// Override the entry point when the derived name does not match. +database.loadExtension('./base64.dylib', 'sqlite3_base64_init'); +``` + ### `database.enableLoadExtension(allow)` + ```mjs import assert from 'node:assert/strict'; @@ -2073,7 +2073,7 @@ assert.ok(0); // assert.ok(0) ``` - + ```cjs const assert = require('node:assert/strict'); diff --git a/doc/api/console.md b/doc/api/console.md index f0669558abe385..804778bd5fa681 100644 --- a/doc/api/console.md +++ b/doc/api/console.md @@ -240,9 +240,7 @@ added: v8.3.0 Maintains an internal counter specific to `label` and outputs to `stdout` the number of times `console.count()` has been called with the given `label`. - - -```js +```console > console.count() default: 1 undefined @@ -274,9 +272,7 @@ added: v8.3.0 Resets the internal counter specific to `label`. - - -```js +```console > console.count('abc'); abc: 1 undefined diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 13ae03dc608740..4948a1f4a03693 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -53,8 +53,6 @@ try { } ``` - - When using the lexical ESM `import` keyword, the error can only be caught if a handler for `process.on('uncaughtException')` is registered _before_ any attempt to load the module is made (using, for instance, diff --git a/doc/api/dns.md b/doc/api/dns.md index 5bb0f83b36729e..7ba830048e013d 100644 --- a/doc/api/dns.md +++ b/doc/api/dns.md @@ -205,14 +205,12 @@ Returns an array of IP address strings, formatted according to [RFC 5952][], that are currently configured for DNS resolution. A string will include a port section if a custom port is used. - - -```js +```json [ - '8.8.8.8', - '2001:4860:4860::8888', - '8.8.8.8:1053', - '[2001:4860:4860::8888]:1053', + "8.8.8.8", + "2001:4860:4860::8888", + "8.8.8.8:1053", + "[2001:4860:4860::8888]:1053", ] ``` @@ -559,8 +557,6 @@ will be present on the object: Here is an example of the `ret` object passed to the callback: - - ```js [ { type: 'A', address: '127.0.0.1', ttl: 299 }, { type: 'CNAME', value: 'example.com' }, @@ -574,7 +570,7 @@ Here is an example of the `ret` object passed to the callback: refresh: 900, retry: 900, expire: 1800, - minttl: 60 } ] + minttl: 60 } ]; ``` DNS server operators may choose not to respond to `ANY` @@ -678,17 +674,15 @@ function will contain an array of objects with the following properties: * `order` * `preference` - - ```js -{ +({ flags: 's', service: 'SIP+D2U', regexp: '', replacement: '_sip._udp.example.com', order: 30, - preference: 100 -} + preference: 100, +}); ``` ## `dns.resolveNs(hostname, callback)` @@ -763,18 +757,16 @@ be an object with the following properties: * `expire` * `minttl` - - ```js -{ +({ nsname: 'ns.example.com', hostmaster: 'root.example.com', serial: 2013101809, refresh: 10000, retry: 2400, expire: 604800, - minttl: 3600 -} + minttl: 3600, +}); ``` ## `dns.resolveSrv(hostname, callback)` @@ -803,15 +795,13 @@ be an array of objects with the following properties: * `port` * `name` - - ```js -{ +({ priority: 10, weight: 5, port: 21223, - name: 'service.example.com' -} + name: 'service.example.com', +}); ``` ## `dns.resolveTlsa(hostname, callback)` @@ -840,15 +830,13 @@ array of objects with these properties: * `match` * `data` - - ```js -{ +({ certUsage: 3, selector: 1, match: 1, - data: [ArrayBuffer] -} + data: [ArrayBuffer], +}); ``` ## `dns.resolveTxt(hostname, callback)` @@ -1081,14 +1069,12 @@ Returns an array of IP address strings, formatted according to [RFC 5952][], that are currently configured for DNS resolution. A string will include a port section if a custom port is used. - - -```js +```json [ - '8.8.8.8', - '2001:4860:4860::8888', - '8.8.8.8:1053', - '[2001:4860:4860::8888]:1053', + "8.8.8.8", + "2001:4860:4860::8888", + "8.8.8.8:1053", + "[2001:4860:4860::8888]:1053" ] ``` @@ -1329,8 +1315,6 @@ present on the object: Here is an example of the result object: - - ```js [ { type: 'A', address: '127.0.0.1', ttl: 299 }, { type: 'CNAME', value: 'example.com' }, @@ -1344,7 +1328,7 @@ Here is an example of the result object: refresh: 900, retry: 900, expire: 1800, - minttl: 60 } ] + minttl: 60 } ]; ``` ### `dnsPromises.resolveCaa(hostname)` @@ -1407,17 +1391,15 @@ of objects with the following properties: * `order` * `preference` - - ```js -{ +({ flags: 's', service: 'SIP+D2U', regexp: '', replacement: '_sip._udp.example.com', order: 30, - preference: 100 -} + preference: 100, +}); ``` ### `dnsPromises.resolveNs(hostname)` @@ -1465,18 +1447,16 @@ following properties: * `expire` * `minttl` - - ```js -{ +({ nsname: 'ns.example.com', hostmaster: 'root.example.com', serial: 2013101809, refresh: 10000, retry: 2400, expire: 604800, - minttl: 3600 -} + minttl: 3600, +}); ``` ### `dnsPromises.resolveSrv(hostname)` @@ -1496,15 +1476,13 @@ the following properties: * `port` * `name` - - ```js -{ +({ priority: 10, weight: 5, port: 21223, - name: 'service.example.com' -} + name: 'service.example.com', +}); ``` ### `dnsPromises.resolveTlsa(hostname)` @@ -1526,15 +1504,13 @@ with these properties: * `match` * `data` - - ```js -{ +({ certUsage: 3, selector: 1, match: 1, - data: [ArrayBuffer] -} + data: [ArrayBuffer], +}); ``` ### `dnsPromises.resolveTxt(hostname)` diff --git a/doc/api/esm.md b/doc/api/esm.md index c3377fc7c29a71..a8b44724573659 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -564,8 +564,6 @@ console.log(cjs === cjsSugar); This Module Namespace Exotic Object can be directly observed either when using `import * as m from 'cjs'` or a dynamic import: - - ```js import * as m from 'cjs'; console.log(m); diff --git a/doc/api/http.md b/doc/api/http.md index f3ae8ea409ce15..f8c74784e2033c 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -39,15 +39,13 @@ property, which is an array of `[key, value, key2, value2, ...]`. For example, the previous message header object might have a `rawHeaders` list like the following: - - -```js -[ 'ConTent-Length', '123456', - 'content-LENGTH', '123', - 'content-type', 'text/plain', - 'CONNECTION', 'keep-alive', - 'Host', 'example.com', - 'accepT', '*/*' ] +```json +[ "ConTent-Length", "123456", + "content-LENGTH", "123", + "content-type", "text/plain", + "CONNECTION", "keep-alive", + "Host", "example.com", + "accepT", "*/*" ] ``` ## Class: `http.Agent` diff --git a/doc/api/http2.md b/doc/api/http2.md index 50aa991401b01f..9080ddcbcdb955 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -4279,10 +4279,8 @@ Accept: text/plain Then `request.url` will be: - - -```js -'/status?name=ryan' +```json +"/status?name=ryan" ``` To parse the url into its parts, `new URL()` can be used: diff --git a/doc/api/module.md b/doc/api/module.md index 446863a178c36a..681b90cebc91e3 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -1149,8 +1149,6 @@ export async function load(url, context, nextLoad) { Unlike synchronous hooks, the asynchronous hooks would not run for these modules loaded in the file that calls `register()`: - - ```mjs // register-hooks.js import { register, createRequire } from 'node:module'; @@ -1158,12 +1156,10 @@ register('./hooks.mjs', import.meta.url); // Asynchronous hooks does not affect modules loaded via custom require() // functions created by module.createRequire(). -const userRequire = createRequire(__filename); +const userRequire = createRequire(import.meta.filename); userRequire('./my-app-2.cjs'); // Hooks won't affect this ``` - - ```cjs // register-hooks.js const { register, createRequire } = require('node:module'); diff --git a/doc/api/modules.md b/doc/api/modules.md index 74279e28182569..5baac055af8677 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -262,8 +262,6 @@ the default export in the `.default` property, similar to the results returned b To customize what should be returned by `require(esm)` directly, the ES Module can export the desired value using the string name `"module.exports"`. - - ```mjs // point.mjs export default class Point { @@ -273,7 +271,7 @@ export default class Point { // `distance` is lost to CommonJS consumers of this module, unless it's // added to `Point` as a static property. export function distance(a, b) { return Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2); } -export { Point as 'module.exports' } +export { Point as 'module.exports' }; ``` @@ -293,8 +291,6 @@ named exports, the module can make sure that the default export is an object wit named exports attached to it as properties. For example with the example above, `distance` can be attached to the default export, the `Point` class, as a static method. - - ```mjs export function distance(a, b) { return Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2); } @@ -303,7 +299,7 @@ export default class Point { static distance = distance; } -export { Point as 'module.exports' } +export { Point as 'module.exports' }; ``` diff --git a/doc/api/os.md b/doc/api/os.md index 9f2aa0390cc56a..6b3bb1ddfa554a 100644 --- a/doc/api/os.md +++ b/doc/api/os.md @@ -94,8 +94,6 @@ The properties included on each object include: * `idle` {number} The number of milliseconds the CPU has spent in idle mode. * `irq` {number} The number of milliseconds the CPU has spent in irq mode. - - ```js [ { @@ -142,7 +140,7 @@ The properties included on each object include: irq: 20, }, }, -] +]; ``` `nice` values are POSIX-only. On Windows, the `nice` values of all processors @@ -298,46 +296,44 @@ The properties available on the assigned network address object include: in CIDR notation. If the `netmask` is invalid, this property is set to `null`. - - -```js +```json { - lo: [ + "lo:": [ { - address: '127.0.0.1', - netmask: '255.0.0.0', - family: 'IPv4', - mac: '00:00:00:00:00:00', - internal: true, - cidr: '127.0.0.1/8' + "address:": "127.0.0.1", + "netmask:": "255.0.0.0", + "family:": "IPv4", + "mac:": "00:00:00:00:00:00", + "internal:": true, + "cidr:": "127.0.0.1/8" }, { - address: '::1', - netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', - family: 'IPv6', - mac: '00:00:00:00:00:00', - scopeid: 0, - internal: true, - cidr: '::1/128' + "address:": "::1", + "netmask:": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "family:": "IPv6", + "mac:": "00:00:00:00:00:00", + "scopeid:": 0, + "internal:": true, + "cidr:": "::1/128" } ], - eth0: [ + "eth0:": [ { - address: '192.168.1.108', - netmask: '255.255.255.0', - family: 'IPv4', - mac: '01:02:03:0a:0b:0c', - internal: false, - cidr: '192.168.1.108/24' + "address:": "192.168.1.108", + "netmask:": "255.255.255.0", + "family:": "IPv4", + "mac:": "01:02:03:0a:0b:0c", + "internal:": false, + "cidr:": "192.168.1.108/24" }, { - address: 'fe80::a00:27ff:fe4e:66a1', - netmask: 'ffff:ffff:ffff:ffff::', - family: 'IPv6', - mac: '01:02:03:0a:0b:0c', - scopeid: 1, - internal: false, - cidr: 'fe80::a00:27ff:fe4e:66a1/64' + "address:": "fe80::a00:27ff:fe4e:66a1", + "netmask:": "ffff:ffff:ffff:ffff::", + "family:": "IPv6", + "mac:": "01:02:03:0a:0b:0c", + "scopeid:": 1, + "internal:": false, + "cidr:": "fe80::a00:27ff:fe4e:66a1/64" } ] } diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index 5c838da9b3f2db..444e2182af0cbf 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -2167,8 +2167,6 @@ setTimeout(() => {}, 1000); The following example measures the duration of `require()` operations to load dependencies: - - ```mjs import { performance, PerformanceObserver } from 'node:perf_hooks'; diff --git a/doc/api/process.md b/doc/api/process.md index 100ff996d82ff1..60f9866faee7dd 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -1105,30 +1105,28 @@ when running the `./configure` script. An example of the possible output looks like: - - -```js +```json { - target_defaults: - { cflags: [], - default_configuration: 'Release', - defines: [], - include_dirs: [], - libraries: [] }, - variables: + "target_defaults": + { "cflags": [], + "default_configuration": "Release", + "defines": [], + "include_dirs": [], + "libraries": [] }, + "variables": { - host_arch: 'x64', - napi_build_version: 5, - node_install_npm: 'true', - node_prefix: '', - node_shared_cares: 'false', - node_shared_http_parser: 'false', - node_shared_libuv: 'false', - node_shared_zlib: 'false', - node_use_openssl: 'true', - node_shared_openssl: 'false', - target_arch: 'x64', - v8_use_snapshot: 1 + "host_arch": "x64", + "napi_build_version": 5, + "node_install_npm": "true", + "node_prefix": "", + "node_shared_cares": "false", + "node_shared_http_parser": "false", + "node_shared_libuv": "false", + "node_shared_zlib": "false", + "node_use_openssl": "true", + "node_shared_openssl": "false", + "target_arch": "x64", + "v8_use_snapshot": 1 } } ``` @@ -1608,20 +1606,18 @@ See environ(7). An example of this object looks like: - - -```js +```json { - TERM: 'xterm-256color', - SHELL: '/usr/local/bin/bash', - USER: 'maciej', - PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin', - PWD: '/Users/maciej', - EDITOR: 'vim', - SHLVL: '1', - HOME: '/Users/maciej', - LOGNAME: 'maciej', - _: '/usr/local/bin/node' + "TERM": "xterm-256color", + "SHELL": "/usr/local/bin/bash", + "USER": "maciej", + "PATH": "~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin", + "PWD": "/Users/maciej", + "EDITOR": "vim", + "SHLVL": "1", + "HOME": "/Users/maciej", + "LOGNAME": "maciej", + "_": "/usr/local/bin/node" } ``` @@ -1750,10 +1746,8 @@ Results in `process.execArgv`: And `process.argv`: - - -```js -['/usr/local/bin/node', 'script.js', '--version'] +```json +["/usr/local/bin/node", "script.js", "--version"] ``` Refer to [`Worker` constructor][] for the detailed behavior of worker @@ -1770,10 +1764,8 @@ added: v0.1.100 The `process.execPath` property returns the absolute pathname of the executable that started the Node.js process. Symbolic links, if any, are resolved. - - -```js -'/usr/local/bin/node' +```json +"/usr/local/bin/node" ``` ## `process.execve(file[, args[, env]])` @@ -3380,15 +3372,13 @@ tarball. * `'Hydrogen'` for the 18.x LTS line beginning with 18.12.0. For other LTS Release code names, see [Node.js Changelog Archive](https://github.com/nodejs/node/blob/HEAD/doc/changelogs/CHANGELOG_ARCHIVE.md) - - -```js +```json { - name: 'node', - lts: 'Hydrogen', - sourceUrl: 'https://nodejs.org/download/release/v18.12.0/node-v18.12.0.tar.gz', - headersUrl: 'https://nodejs.org/download/release/v18.12.0/node-v18.12.0-headers.tar.gz', - libUrl: 'https://nodejs.org/download/release/v18.12.0/win-x64/node.lib' + "name": "node", + "lts": "Hydrogen", + "sourceUrl": "https://nodejs.org/download/release/v18.12.0/node-v18.12.0.tar.gz", + "headersUrl": "https://nodejs.org/download/release/v18.12.0/node-v18.12.0-headers.tar.gz", + "libUrl": "https://nodejs.org/download/release/v18.12.0/win-x64/node.lib" } ``` diff --git a/doc/api/stream.md b/doc/api/stream.md index eac441dbd065a1..f9ada690eb7c58 100644 --- a/doc/api/stream.md +++ b/doc/api/stream.md @@ -3573,8 +3573,6 @@ of the four basic stream classes (`stream.Writable`, `stream.Readable`, `stream.Duplex`, or `stream.Transform`), making sure they call the appropriate parent class constructor: - - ```js const { Writable } = require('node:stream'); diff --git a/doc/api/v8.md b/doc/api/v8.md index 523225f0e42013..cafc59c2de797d 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -54,14 +54,12 @@ following properties: * `external_script_source_size` {number} * `cpu_profiler_metadata_size` {number} - - -```js +```json { - code_and_metadata_size: 212208, - bytecode_and_metadata_size: 161368, - external_script_source_size: 1410794, - cpu_profiler_metadata_size: 0, + "code_and_metadata_size": 212208, + "bytecode_and_metadata_size": 161368, + "external_script_source_size": 1410794, + "cpu_profiler_metadata_size": 0 } ``` @@ -263,24 +261,22 @@ used memory size of V8 global handles. `external_memory` The value of external\_memory is the memory size of array buffers and external strings. - - -```js +```json { - total_heap_size: 7326976, - total_heap_size_executable: 4194304, - total_physical_size: 7326976, - total_available_size: 1152656, - used_heap_size: 3476208, - heap_size_limit: 1535115264, - malloced_memory: 16384, - peak_malloced_memory: 1127496, - does_zap_garbage: 0, - number_of_native_contexts: 1, - number_of_detached_contexts: 0, - total_global_handles_size: 8192, - used_global_handles_size: 3296, - external_memory: 318824 + "total_heap_size": 7326976, + "total_heap_size_executable": 4194304, + "total_physical_size": 7326976, + "total_available_size": 1152656, + "used_heap_size": 3476208, + "heap_size_limit": 1535115264, + "malloced_memory": 16384, + "peak_malloced_memory": 1127496, + "does_zap_garbage": 0, + "number_of_native_contexts": 1, + "number_of_detached_contexts": 0, + "total_global_handles_size": 8192, + "used_global_handles_size": 3296, + "external_memory": 318824 } ``` diff --git a/doc/api/vm.md b/doc/api/vm.md index 16cc536c8818c8..9b15254f4f915b 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -1097,8 +1097,6 @@ import foo from 'foo'; import source Foo from 'foo'; ``` - - The `modules` array must contain two references to the same instance, because the two module requests are identical but in two phases. @@ -1138,8 +1136,6 @@ import withAttrs from '../with-attrs.ts' with { arbitraryAttr: 'attr-val' }; import source Module from 'wasm-mod.wasm'; ``` - - The value of the `sourceTextModule.moduleRequests` will be: ```js diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index b8d1c9f8868a73..def3520ac5d920 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -1390,8 +1390,6 @@ not preserved. In particular, {Buffer} objects will be read as plain {Uint8Array}s on the receiving side, and instances of JavaScript classes will be cloned as plain JavaScript objects. - - ```js const b = Symbol('b'); @@ -1402,7 +1400,7 @@ class Foo { this.c = 3; } - get d() { return 4; } + get d() { return this.#a + 3; } } const { port1, port2 } = new MessageChannel(); diff --git a/doc/api/zlib.md b/doc/api/zlib.md index 4ef24328ca6ec0..18a2c07ed1f21d 100644 --- a/doc/api/zlib.md +++ b/doc/api/zlib.md @@ -454,10 +454,8 @@ From `zlib/zconf.h`, modified for Node.js usage: The memory requirements for deflate are (in bytes): - - ```js -(1 << (windowBits + 2)) + (1 << (memLevel + 9)) +(1 << (windowBits + 2)) + (1 << (memLevel + 9)); ``` That is: 128K for `windowBits` = 15 + 128K for `memLevel` = 8 diff --git a/doc/contributing/erm-guidelines.md b/doc/contributing/erm-guidelines.md index b35e7d1bb2d98d..d57acbb8bba282 100644 --- a/doc/contributing/erm-guidelines.md +++ b/doc/contributing/erm-guidelines.md @@ -280,25 +280,25 @@ The `Symbol.dispose` method should return `undefined` and the `Symbol.asyncDispose` method should return a `Promise` that resolves to `undefined`. - + ```js -[Symbol.dispose]() { - return void this.dispose(); - // or - this.dispose(); - // or - return; - // or - // no return -} +class MyIterable { + [Symbol.dispose]() { + this.dispose(); + // or + return; + // or + // no return + } + + async [Symbol.asyncDispose]() { + await this.dispose(); + // or -async [Symbol.asyncDispose]() { - await this.dispose(); - // or - return; - // or - // no return + // or + // no return + } } ``` @@ -312,21 +312,21 @@ directly. For example: - + ```js // Do something like this: -function dispose() { ... } +function dispose() { /* ... */ } return { dispose, - [Symbol.dispose]() { this.dispose(); } + [Symbol.dispose]() { this.dispose(); }, }; // Rather than this: -function dispose() { ... } +function dispose() { /* ... */ } return { dispose, - [Symbol.dispose]: dispose + [Symbol.dispose]: dispose, }; ``` diff --git a/doc/contributing/maintaining/maintaining-icu.md b/doc/contributing/maintaining/maintaining-icu.md index 00992258ae2611..e83aa8a5b0c407 100644 --- a/doc/contributing/maintaining/maintaining-icu.md +++ b/doc/contributing/maintaining/maintaining-icu.md @@ -132,8 +132,6 @@ make test-ci Also running - - ```js new Intl.DateTimeFormat('es', { month: 'long' }).format(new Date(9E8)); ``` @@ -159,8 +157,6 @@ make * Test this newly default-generated Node.js - - ```js process.versions.icu; new Intl.DateTimeFormat('es', { month: 'long' }).format(new Date(9E8)); diff --git a/doc/contributing/using-internal-errors.md b/doc/contributing/using-internal-errors.md index b24c96d9ccd243..db63abfe28a41f 100644 --- a/doc/contributing/using-internal-errors.md +++ b/doc/contributing/using-internal-errors.md @@ -65,17 +65,15 @@ It is possible to create multiple derived classes by providing additional arguments. The other ones will be exposed as properties of the main class: - - ```js E('EXAMPLE_KEY', 'Error message', TypeError, RangeError); // In another module +const assert = require('node:assert'); const { EXAMPLE_KEY } = require('internal/errors').codes; -// TypeError -throw new EXAMPLE_KEY(); -// RangeError -throw new EXAMPLE_KEY.RangeError(); + +assert.throws(() => { throw new EXAMPLE_KEY(); }, { name: 'TypeError' }); +assert.throws(() => { throw new EXAMPLE_KEY.RangeError(); }, { name: 'RangeError' }); ``` ## Documenting new errors From bee964d2d87866ea4b666d18d27a085bc08c9e64 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 11 May 2026 09:36:07 +0200 Subject: [PATCH 13/60] meta: ignore AI assistants files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ignore CLAUDE.md and AGENTS.md in .gitignore, and exclude them from markdown and ESLint linting. Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/62612 Reviewed-By: James M Snell Reviewed-By: Moshe Atlow Reviewed-By: Marco Ippolito Reviewed-By: Ulises Gascón Reviewed-By: Luigi Pinca Reviewed-By: Daijiro Wachi Reviewed-By: Paolo Insogna Reviewed-By: Trivikram Kamat --- .gitignore | 4 ++++ Makefile | 2 +- eslint.config.mjs | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 221e4f4062486a..234bc0e262fb93 100644 --- a/.gitignore +++ b/.gitignore @@ -158,6 +158,10 @@ cmake_install.cmake install_manifest.txt *.cbp +# === Rules for AI assistants === +CLAUDE.md +AGENTS.md + # === Global Rules === # Keep last to avoid being excluded *.pyc diff --git a/Makefile b/Makefile index 1dbb9f95206aa6..1d1ac35b476834 100644 --- a/Makefile +++ b/Makefile @@ -1384,7 +1384,7 @@ else LINT_MD_NEWER = -newer tools/.mdlintstamp endif -LINT_MD_TARGETS = doc src lib benchmark test tools/doc tools/icu $(wildcard *.md) +LINT_MD_TARGETS = doc src lib benchmark test tools/doc tools/icu $(filter-out CLAUDE.md AGENTS.md,$(wildcard *.md)) LINT_MD_FILES = $(shell $(FIND) $(LINT_MD_TARGETS) -type f \ ! -path '*node_modules*' ! -path 'test/fixtures/*' -name '*.md' \ $(LINT_MD_NEWER)) diff --git a/eslint.config.mjs b/eslint.config.mjs index 4b76072dde46cc..9496836803666b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -394,6 +394,7 @@ export default [ // #region markdown config { files: ['**/*.md'], + ignores: ['CLAUDE.md', 'AGENTS.md'], plugins: { markdown, }, From 9283e9204d6402d6a22bacd548f254bce3343431 Mon Sep 17 00:00:00 2001 From: RoomWithOutRoof <166608075+Jah-yee@users.noreply.github.com> Date: Mon, 11 May 2026 15:36:19 +0800 Subject: [PATCH 14/60] lib: narrow ReadableStreamBYOBRequest.view return type to Uint8Array Follow WHATWG streams spec update: https://github.com/whatwg/streams/pull/1367 ReadableStreamBYOBRequest.view is always constructed as a Uint8Array. This changes the documented return type from ArrayBufferView to Uint8Array per the updated spec. Fixes: https://github.com/nodejs/node/issues/62952 Signed-off-by: Jah-yee <166608075+Jah-yee@users.noreply.github.com> PR-URL: https://github.com/nodejs/node/pull/63017 Reviewed-By: Mattias Buelens Reviewed-By: Jason Zhang --- lib/internal/webstreams/readablestream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index 7abde514cf78a5..876e3a5bf6e2f0 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -665,7 +665,7 @@ class ReadableStreamBYOBRequest { /** * @readonly - * @type {ArrayBufferView} + * @type {Uint8Array} */ get view() { if (!isReadableStreamBYOBRequest(this)) From 34c962b6bd176a899c0397652cad3e7b52bf3924 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 11 May 2026 10:45:18 +0200 Subject: [PATCH 15/60] doc: add large pull requests contributing guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Exclude routine dependency/WPT/bot PRs from the policy - Replace design document requirement with detailed PR description - Clarify dependency commit ordering for squash landing - Remove splitting strategies that contradict self-contained PRs - Add links from CONTRIBUTING.md, pull-requests.md, collaborator-guide.md Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/62829 Fixes: https://github.com/nodejs/node/issues/62752 Reviewed-By: James M Snell Reviewed-By: Trivikram Kamat Reviewed-By: Rafael Gonzaga Reviewed-By: Yagiz Nizipli Reviewed-By: Chengzhong Wu Reviewed-By: Paolo Insogna Reviewed-By: Marco Ippolito Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Ruy Adorno --- CONTRIBUTING.md | 1 + doc/contributing/collaborator-guide.md | 4 + doc/contributing/large-pull-requests.md | 182 ++++++++++++++++++++++++ doc/contributing/pull-requests.md | 6 +- 4 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 doc/contributing/large-pull-requests.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54296234a304d8..e22f23543e5ccc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,7 @@ dependencies, and tools contained in the `nodejs/node` repository. * [Setting up your local environment](./doc/contributing/pull-requests.md#setting-up-your-local-environment) * [The Process of Making Changes](./doc/contributing/pull-requests.md#the-process-of-making-changes) * [Reviewing Pull Requests](./doc/contributing/pull-requests.md#reviewing-pull-requests) +* [Large Pull Requests](./doc/contributing/large-pull-requests.md) * [Notes](./doc/contributing/pull-requests.md#notes) ## Automation and bots diff --git a/doc/contributing/collaborator-guide.md b/doc/contributing/collaborator-guide.md index b3e201751b2280..8f29042688ef6e 100644 --- a/doc/contributing/collaborator-guide.md +++ b/doc/contributing/collaborator-guide.md @@ -132,6 +132,9 @@ Pay special attention to pull requests for dependencies which have not been automatically generated and follow the guidance in [Maintaining Dependencies](https://github.com/nodejs/node/blob/main/doc/contributing/maintaining/maintaining-dependencies.md#updating-dependencies). +Pull requests that exceed 5000 lines of changes have additional requirements. +See the [large pull requests][] guide. + In some cases, it might be necessary to summon a GitHub team to a pull request for review by @-mention. See [Who to CC in the issue tracker](#who-to-cc-in-the-issue-tracker). @@ -1068,6 +1071,7 @@ need to be attached anymore, as only important bugfixes will be included. [git-node]: https://github.com/nodejs/node-core-utils/blob/HEAD/docs/git-node.md [git-node-metadata]: https://github.com/nodejs/node-core-utils/blob/HEAD/docs/git-node.md#git-node-metadata [git-username]: https://help.github.com/articles/setting-your-username-in-git/ +[large pull requests]: large-pull-requests.md [macos]: https://github.com/orgs/nodejs/teams/platform-macos [node-core-utils-credentials]: https://github.com/nodejs/node-core-utils#setting-up-credentials [node-core-utils-issues]: https://github.com/nodejs/node-core-utils/issues diff --git a/doc/contributing/large-pull-requests.md b/doc/contributing/large-pull-requests.md new file mode 100644 index 00000000000000..00ceb8452e2abc --- /dev/null +++ b/doc/contributing/large-pull-requests.md @@ -0,0 +1,182 @@ +# Large pull requests + +* [Overview](#overview) +* [What qualifies as a large pull request](#what-qualifies-as-a-large-pull-request) +* [Who can open a large pull request](#who-can-open-a-large-pull-request) +* [Requirements](#requirements) + * [Detailed pull request description](#detailed-pull-request-description) + * [Review guide](#review-guide) + * [Approval requirements](#approval-requirements) + * [Dependency changes](#dependency-changes) +* [Splitting large pull requests](#splitting-large-pull-requests) + * [Feature forks and branches](#feature-forks-and-branches) +* [Guidance for reviewers](#guidance-for-reviewers) + +## Overview + +Large pull requests are difficult to review or sometimes impossible to review in the GitHub UI. They are likely to sit +for a long time without receiving adequate review, and when they do get reviewed, +the quality of that review is often lower due to reviewer fatigue. Contributors +should avoid creating large pull requests except in those cases where it is +Large pull requests are difficult to review or sometimes impossible to review +in the GitHub UI. They are likely to sit for a long time without receiving +adequate review, and when they do get reviewed, the quality of that review is +often lower due to reviewer fatigue. Contributors should avoid creating large +pull requests except in those cases where it is effectively unavoidable, such +as when adding new dependencies. + +This document outlines the policy for authoring and reviewing large pull +requests in the Node.js project. + +## What qualifies as a large pull request + +A pull request is considered large when it exceeds **5000 lines** of net +change (lines added minus lines deleted). This threshold applies across all +files in the pull request, including changes in `deps/`, `test/`, `doc/`, +`lib/`, `src/`, and `tools/`. + +Any pull request that adds a new subsystem, e.g. `node:foo` or `node:foo/bar`, +is automatically considered a large pull request and subject to the same rules. + +Changes in `deps/` are included in this count. Dependency changes are +sensitive because they often receive less scrutiny than first-party code. + +The following categories of pull requests are **excluded** from this policy, +even if they exceed the line threshold: + +* Routine dependency updates (e.g., V8, ICU, undici, uvwasi) generated by + automation or performed by collaborators following the standard dependency + update process. +* Web Platform Tests (WPT) imports and updates. +* Other bot-issued or automated pull requests (e.g., license updates, test + fixture regeneration). +* Test-only refactoring that involves no functional changes. + These pull requests already have established review processes and do not + benefit from the additional requirements described here. + +## Who can open a large pull request + +Large pull requests may only be opened by existing +[collaborators](https://github.com/nodejs/node/#current-project-team-members). +Non-collaborators are strongly discouraged from opening pull requests of this size. +Large pull requests from non-collaborators will be closed unless it has been discussed +in an issue and has a collaborator to champion the work. + +## Requirements + +All large pull requests must satisfy the following requirements in addition to +the standard [pull request requirements](./pull-requests.md). + +### Detailed pull request description + +The pull request description must provide sufficient context for reviewers +to understand the change. The description should explain: + +* The motivation for the change. +* The high-level approach and architecture. +* Any alternatives that were considered and why they were rejected. +* How the change interacts with existing subsystems. + +A thorough pull request description is sufficient. There is no requirement +to produce a separate design document, although contributors may choose to +link to a GitHub issue or other discussion where the design was developed. + +### Review guide + +The pull request description must include a review guide that helps reviewers +navigate the change. The review guide should: + +* Identify the key files and directories to review. +* Describe the order in which files should be reviewed. +* Highlight the most critical sections that need careful attention. +* Include a testing plan explaining how the change has been validated and + how reviewers can verify the behavior. + +### Approval requirements + +Large pull requests follow the same approval path as semver-major changes: + +* At least **two TSC member approvals** are required. +* The standard 48-hour wait time applies. Given the complexity of large pull + requests, authors should expect and allow for a longer review period. +* CI must pass before landing. + +### Dependency changes + +When a large pull request adds or modifies a dependency in `deps/`: + +* Dependency changes should be in a **separate commit** from the rest of the + pull request. This makes it easier to review the dependency update + independently from the first-party code changes. When the pull request is + squashed on landing, the dependency commit should be the one that carries + the squashed commit message, so that `git log` clearly reflects the + overall change. +* The provenance and integrity of the dependency must be verifiable. + Include documentation of how the dependency was obtained and how + reviewers can reproduce the build artifact. + +## Avoiding large pull requests + +Contributors should always consider whether a large pull request can be split +into smaller, independently reviewable pull requests. Strategies include: + +* Landing foundational internal APIs first, then building on top of them. +* Landing refactoring or preparatory changes before the main feature. + +Each pull request in a split series should remain self-contained: it should +include the implementation, tests, and documentation needed for that piece +to stand on its own. + +### Strategies for reducing the review length in single pull requests + +Large pull requests may involve a longer review process that becomes practically +impossible to track on GitHub due to UI limitations. These strategies help reduce +the review length in a single pull request. + +* Open an issue first to confirm a substantial change is indeed desired in core + to reduce lengthy discussions unrelated to the implementation in the pull request. +* Use proposal issues, RFCs, design documents, or other types of venues to + explore high-level design and cross-cutting concerns. +* Keep the initial change provisional to reduce the thoroughness required in a + single pull request. Gate premature changes behind build/runtime flags, or apply + `dont-land-*` labels to avoid releasing the initial changes until it has been more + thoroughly tested and iterated in follow-up pull requests. +* Leave non-blocking issues (e.g. stylistic preferences) to follow-up pull requests + with a TODO comment in appropriate places. + +### Feature forks and branches + +For extremely large or complex changes that develop over time, such as adding +a major new subsystem, contributors should consider using a feature fork. +This approach has been used successfully in the past for subsystems like QUIC. + +The feature fork must be hosted in a **separate GitHub repository**, managed +by the collaborator championing the change. The repository can live in the +[nodejs organization](https://github.com/nodejs) or be a personal repository +of the champion. The champion is responsible for coordinating development, +managing access, and ensuring the fork stays up to date with `main`. + +A feature fork allows: + +* Incremental development with multiple collaborators. +* Review of individual commits rather than one monolithic diff. +* CI validation at each stage of development. +* Independent issue tracking and discussion in the fork repository. + +When the work is ready, the final merge into `main` via a pull request still +requires the same approval and review requirements as any other large pull +request. + +## Guidance for reviewers + +Reviewing a large pull request is a significant time investment. Reviewers +should: + +* Read the pull request description and review guide before diving into the + code. +* Focus review effort on `lib/` and `src/` changes, which have the highest + impact on the runtime. `test/` and `doc/` changes, while important, are + lower risk. +* Not hesitate to request that the author split the pull request if it can + reasonably be broken into smaller pieces. +* Coordinate with other reviewers to divide the review workload when possible. diff --git a/doc/contributing/pull-requests.md b/doc/contributing/pull-requests.md index 0fcf4c339119b8..0893460cbc8b0d 100644 --- a/doc/contributing/pull-requests.md +++ b/doc/contributing/pull-requests.md @@ -187,7 +187,7 @@ A good commit message should describe what changed and why. `Fixes:` and `Refs:` trailers get automatically added to your commit message when the Pull Request lands as long as they are included in the Pull Request's description. If the Pull Request lands in several commits, - by default the trailers found in the description are added to each commit. + by default the trailers found in the description are added to each commits. Examples: @@ -289,6 +289,9 @@ From within GitHub, opening a new pull request will present you with a [pull request template][]. Please try to do your best at filling out the details, but feel free to skip parts if you're not sure what to put. +If your pull request exceeds 5000 lines of changes, see the +[large pull requests][] guide for additional requirements. + Once opened, pull requests are usually reviewed within a few days. To get feedback on your proposed change even though it is not ready @@ -611,6 +614,7 @@ More than one subsystem may be valid for any particular issue or pull request. [guide for writing tests in Node.js]: writing-tests.md [hiding-a-comment]: https://help.github.com/articles/managing-disruptive-comments/#hiding-a-comment [https://ci.nodejs.org/]: https://ci.nodejs.org/ +[large pull requests]: large-pull-requests.md [maintaining dependencies]: ./maintaining/maintaining-dependencies.md [nodejs/core-validate-commit]: https://github.com/nodejs/core-validate-commit/blob/main/lib/rules/subsystem.js [pull request template]: https://raw.githubusercontent.com/nodejs/node/HEAD/.github/PULL_REQUEST_TEMPLATE.md From 8820f642853196660ee768767a242af0f4d0e1ba Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 11 May 2026 17:36:41 +0200 Subject: [PATCH 16/60] tools: use different branch for tool updates on staging branches Signed-off-by: Antoine du Hamel PR-URL: https://github.com/nodejs/node/pull/63110 Reviewed-By: James M Snell Reviewed-By: Richard Lau Reviewed-By: Marco Ippolito --- .github/workflows/tools.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml index 638a0fc2142118..59c9d4bcd466e7 100644 --- a/.github/workflows/tools.yml +++ b/.github/workflows/tools.yml @@ -311,7 +311,7 @@ jobs: # no-op if the base branch is already up-to-date. with: token: ${{ secrets.GH_USER_TOKEN }} - branch: actions/tools-update-${{ matrix.id }} # Custom branch *just* for this Action. + branch: actions/${{ github.ref_name == 'main' || format('{0}/', github.ref_name) }}tools-update-${{ matrix.id }} # Custom branch *just* for this Action. delete-branch: true commit-message: ${{ env.COMMIT_MSG }} labels: ${{ matrix.label }} From 2f384f91ecf63efb19913209c0808e216a741afd Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 11 May 2026 23:54:19 +0200 Subject: [PATCH 17/60] debugger: add --help to `node inspect` and improve docs - Add `--help` / `-h` to `node inspect` covering both interactive and non-interactive probe modes. The help text is printed when `--help`/`-h` appears before any positional argument to avoid hijacking `--help` passed to a child script. - Improve the documentation of probe mode and add examples, explain same-location probe coalescing, TDZ caveat for let/const bindings, basename matching and exit code behavior. Also move it to a section parallel to interactive mode. Remove recommendation of evaluating structured expressions as that is prone to missing info in JSON mode. Drive-by: When probe mode exits due to invalid arguments, exit with `kInvalidCommandLineArgument` (9) instead of `kGenericUserError` (1). Signed-off-by: Joyee Cheung PR-URL: https://github.com/nodejs/node/pull/63201 Reviewed-By: Jan Martin Reviewed-By: Chengzhong Wu Reviewed-By: Aviv Keller --- doc/api/debugger.md | 387 ++++++++++++------ lib/internal/debugger/inspect.js | 77 +++- lib/internal/debugger/inspect_helpers.js | 74 +++- lib/internal/debugger/inspect_probe.js | 64 +-- .../test-debugger-inspect-help-forwarding.mjs | 34 ++ test/parallel/test-debugger-inspect-help.js | 37 ++ .../test-debugger-probe-missing-expr.js | 2 +- .../test-debugger-probe-requires-separator.js | 2 +- 8 files changed, 460 insertions(+), 217 deletions(-) create mode 100644 test/parallel/test-debugger-inspect-help-forwarding.mjs create mode 100644 test/parallel/test-debugger-inspect-help.js diff --git a/doc/api/debugger.md b/doc/api/debugger.md index 68fdf255a7ecd4..2fb579ca967d67 100644 --- a/doc/api/debugger.md +++ b/doc/api/debugger.md @@ -10,6 +10,14 @@ Node.js includes a command-line debugging utility. The Node.js debugger client is not a full-featured debugger, but simple stepping and inspection are possible. +The debugger supports two modes of operation: [interactive mode][] and [non-interactive probe mode][]. + +## Interactive mode + +```console +$ node inspect [--port=] [ ...] [