From 9d62d0bc74ba3489cf02057f761f720ada8c4d30 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sun, 22 Mar 2026 04:17:32 +0100 Subject: [PATCH 1/3] build: make test-addons dependency-free `make test-addons` used to depend on a markdown parser and then doc-kit to extract C++ addon examples from addons.md by guessing the file contents based on headings. This is hacky and brittle. The introduction of doc-kit also means tests intended for verifying the binary like `make test-only` now need to support doc-building toolchains e.g. minifier, highlighter, and indirect dependencies that rely on prebuilt-addon/wasm, which defeats the purpose and makes it harder to run for experimental platforms. This patch adds explicit `` markers in addons.md to locate extractable code blocks, avoiding fragile heuristics based on heading text or code block order and eliminating the dependency with simpler parsing. --- Makefile | 10 ++--- doc/api/addons.md | 66 +++++++++++++++++++++++++++++++ tools/doc/addon-verify.mjs | 79 ++++++++++++++++++++++++++++++++++++++ vcbuild.bat | 2 +- 4 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 tools/doc/addon-verify.mjs diff --git a/Makefile b/Makefile index 284e1d7eb548c3..04da5817324f8b 100644 --- a/Makefile +++ b/Makefile @@ -388,17 +388,15 @@ DOC_KIT ?= tools/doc/node_modules/@nodejs/doc-kit/bin/cli.mjs node_use_openssl_and_icu = $(call available-node,"-p" \ "process.versions.openssl != undefined && process.versions.icu != undefined") -test/addons/.docbuildstamp: $(DOCBUILDSTAMP_PREREQS) tools/doc/node_modules +test/addons/.docbuildstamp: $(DOCBUILDSTAMP_PREREQS) tools/doc/addon-verify.mjs @if [ "$(shell $(node_use_openssl_and_icu))" != "true" ]; then \ echo "Skipping .docbuildstamp (no crypto and/or no ICU)"; \ else \ $(RM) -r test/addons/??_*/; \ $(call available-node, \ - $(DOC_KIT) generate \ - -t addon-verify \ - -i doc/api/addons.md \ - -o test/addons/ \ - --type-map doc/type-map.json \ + tools/doc/addon-verify.mjs \ + --input doc/api/addons.md \ + --output test/addons/ \ ) \ [ $$? -eq 0 ] && touch $@; \ fi diff --git a/doc/api/addons.md b/doc/api/addons.md index b4d25128235896..7cb237854249e9 100644 --- a/doc/api/addons.md +++ b/doc/api/addons.md @@ -280,6 +280,9 @@ such as any libuv handles registered by the addon. The following `addon.cc` uses `AddEnvironmentCleanupHook`: + + + ```cpp // addon.cc #include @@ -328,6 +331,9 @@ NODE_MODULE_INIT(/* exports, module, context */) { Test in JavaScript by running: + + + ```js // test.js require('./build/Release/addon'); @@ -526,6 +532,9 @@ code. The following example illustrates how to read function arguments passed from JavaScript and how to return a result: + + + ```cpp // addon.cc #include @@ -585,6 +594,9 @@ NODE_MODULE(NODE_GYP_MODULE_NAME, Init) Once compiled, the example addon can be required and used from within Node.js: + + + ```js // test.js const addon = require('./build/Release/addon'); @@ -598,6 +610,9 @@ It is common practice within addons to pass JavaScript functions to a C++ function and execute them from there. The following example illustrates how to invoke such callbacks: + + + ```cpp // addon.cc #include @@ -641,6 +656,9 @@ property of `exports`. To test it, run the following JavaScript: + + + ```js // test.js const addon = require('./build/Release/addon'); @@ -659,6 +677,9 @@ Addons can create and return new objects from within a C++ function as illustrated in the following example. An object is created and returned with a property `msg` that echoes the string passed to `createObject()`: + + + ```cpp // addon.cc #include @@ -698,6 +719,9 @@ NODE_MODULE(NODE_GYP_MODULE_NAME, Init) To test it in JavaScript: + + + ```js // test.js const addon = require('./build/Release/addon'); @@ -713,6 +737,9 @@ console.log(obj1.msg, obj2.msg); Another common scenario is creating JavaScript functions that wrap C++ functions and returning those back to JavaScript: + + + ```cpp // addon.cc #include @@ -760,6 +787,9 @@ NODE_MODULE(NODE_GYP_MODULE_NAME, Init) To test: + + + ```js // test.js const addon = require('./build/Release/addon'); @@ -774,6 +804,9 @@ console.log(fn()); It is also possible to wrap C++ objects/classes in a way that allows new instances to be created using the JavaScript `new` operator: + + + ```cpp // addon.cc #include @@ -795,6 +828,9 @@ NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll) Then, in `myobject.h`, the wrapper class inherits from `node::ObjectWrap`: + + + ```cpp // myobject.h #ifndef MYOBJECT_H @@ -828,6 +864,9 @@ In `myobject.cc`, implement the various methods that are to be exposed. In the following code, the method `plusOne()` is exposed by adding it to the constructor's prototype: + + + ```cpp // myobject.cc #include "myobject.h" @@ -931,6 +970,9 @@ To build this example, the `myobject.cc` file must be added to the Test it with: + + + ```js // test.js const addon = require('./build/Release/addon'); @@ -968,6 +1010,9 @@ const obj = addon.createObject(); First, the `createObject()` method is implemented in `addon.cc`: + + + ```cpp // addon.cc #include @@ -1001,6 +1046,9 @@ In `myobject.h`, the static method `NewInstance()` is added to handle instantiating the object. This method takes the place of using `new` in JavaScript: + + + ```cpp // myobject.h #ifndef MYOBJECT_H @@ -1033,6 +1081,9 @@ class MyObject : public node::ObjectWrap { The implementation in `myobject.cc` is similar to the previous example: + + + ```cpp // myobject.cc #include @@ -1147,6 +1198,9 @@ Once again, to build this example, the `myobject.cc` file must be added to the Test it with: + + + ```js // test.js const createObject = require('./build/Release/addon'); @@ -1175,6 +1229,9 @@ wrapped objects around by unwrapping them with the Node.js helper function `node::ObjectWrap::Unwrap`. The following examples shows a function `add()` that can take two `MyObject` objects as input arguments: + + + ```cpp // addon.cc #include @@ -1224,6 +1281,9 @@ NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll) In `myobject.h`, a new public method is added to allow access to private values after unwrapping the object. + + + ```cpp // myobject.h #ifndef MYOBJECT_H @@ -1256,6 +1316,9 @@ class MyObject : public node::ObjectWrap { The implementation of `myobject.cc` remains similar to the previous version: + + + ```cpp // myobject.cc #include @@ -1340,6 +1403,9 @@ void MyObject::NewInstance(const FunctionCallbackInfo& args) { Test it with: + + + ```js // test.js const addon = require('./build/Release/addon'); diff --git a/tools/doc/addon-verify.mjs b/tools/doc/addon-verify.mjs new file mode 100644 index 00000000000000..2daadbaf44289f --- /dev/null +++ b/tools/doc/addon-verify.mjs @@ -0,0 +1,79 @@ +#!/usr/bin/env node + +// Extracts C++ addon examples from doc/api/addons.md into numbered test +// directories under test/addons/. +// +// Each code block to extract is preceded by a marker in the markdown: +// +// +// ```cpp +// #include +// ... +// ``` +// +// This produces test/addons/01_worker_support/addon.cc. +// Sections are numbered in order of first appearance. + +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { parseArgs } from 'node:util'; + +const { values } = parseArgs({ + options: { + input: { type: 'string' }, + output: { type: 'string' }, + }, +}); + +if (!values.input || !values.output) { + console.error('Usage: addon-verify.mjs --input --output '); + process.exit(1); +} + +const src = readFileSync(values.input, 'utf8'); + +// Collect files grouped by section directory name. +const MARKER_RE = /^$/gm; +const FENCE_RE = /```\w*\n([\s\S]*?)\n```/; +const entries = []; +for (const match of src.matchAll(MARKER_RE)) { + const [, dir, name] = match; + const after = src.slice(match.index + match[0].length); + const content = after.match(FENCE_RE)?.[1]; + if (content != null) entries.push({ dir, name, content }); +} +const sections = Map.groupBy(entries, (e) => e.dir); + +let idx = 0; +for (const [name, files] of sections) { + const dirName = `${String(++idx).padStart(2, '0')}_${name}`; + const dir = join(values.output, dirName); + mkdirSync(dir, { recursive: true }); + + for (const file of files) { + let content = file.content; + if (file.name === 'test.js') { + content = + "'use strict';\n" + + "const common = require('../../common');\n" + + content.replace( + "'./build/Release/addon'", + // eslint-disable-next-line no-template-curly-in-string + '`./build/${common.buildType}/addon`', + ); + } + writeFileSync(join(dir, file.name), content); + } + + // Generate binding.gyp + const names = files.map((f) => f.name); + writeFileSync(join(dir, 'binding.gyp'), JSON.stringify({ + targets: [{ + target_name: 'addon', + sources: names, + includes: ['../common.gypi'], + }], + })); + + console.log(`Generated ${dirName} with files: ${names.join(', ')}`); +} diff --git a/vcbuild.bat b/vcbuild.bat index 9b37892ba6f53d..0e058ab7f19b4f 100644 --- a/vcbuild.bat +++ b/vcbuild.bat @@ -670,7 +670,7 @@ for /d %%F in (test\addons\??_*) do ( rd /s /q %%F ) :: generate -%doc_kit_exe% generate -t addon-verify -i "file://%~dp0doc\api\addons.md" -o "file://%~dp0test\addons" --type-map "file://%~dp0doc\type-map.json" +"%node_exe%" tools\doc\addon-verify.mjs --input "%~dp0doc\api\addons.md" --output "%~dp0test\addons" if %errorlevel% neq 0 exit /b %errorlevel% :: building addons setlocal From eb3d3d9f8a7d688ca494677726345a12186cc802 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 23 Mar 2026 13:30:46 +0100 Subject: [PATCH 2/3] fixup! build: make test-addons dependency-free Co-authored-by: Antoine du Hamel --- tools/doc/addon-verify.mjs | 40 +++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/tools/doc/addon-verify.mjs b/tools/doc/addon-verify.mjs index 2daadbaf44289f..670d86b24f3883 100644 --- a/tools/doc/addon-verify.mjs +++ b/tools/doc/addon-verify.mjs @@ -30,18 +30,40 @@ if (!values.input || !values.output) { process.exit(1); } -const src = readFileSync(values.input, 'utf8'); +const src = await open(values.input, 'r'); + +const MARKER_RE = /^$/; -// Collect files grouped by section directory name. -const MARKER_RE = /^$/gm; -const FENCE_RE = /```\w*\n([\s\S]*?)\n```/; const entries = []; -for (const match of src.matchAll(MARKER_RE)) { - const [, dir, name] = match; - const after = src.slice(match.index + match[0].length); - const content = after.match(FENCE_RE)?.[1]; - if (content != null) entries.push({ dir, name, content }); +let nextBlockIsAddonVerifyFile = false; +let expectedClosingFenceMarker; +for await (const line of src.readLines()) { + if (expectedClosingFenceMarker) { + // We're inside a Addon snippet + if (line === expectedClosingFenceMarker) { + // End of the snippet + expectedClosingFenceMarker = null; + continue; + } + + entries.at(-1).content += `${line}\n`; + } + if (nextBlockIsAddonVerifyFile) { + if (line) { + expectedClosingFenceMarker = line.replace(/\w/g, ''); + nextBlockIsAddonVerifyFile = false; + } + continue; + } + const match = MARKER_RE.exec(line); + if (match) { + nextBlockIsAddonVerifyFile = true; + const [, dir, name] = match; + entries.push({ dir, name, content: '' }); + } } + +// Collect files grouped by section directory name. const sections = Map.groupBy(entries, (e) => e.dir); let idx = 0; From 7c9c28dd57392887e278445ec306471372de699c Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 23 Mar 2026 15:25:36 +0100 Subject: [PATCH 3/3] fixup! build: make test-addons dependency-free Co-authored-by: Antoine du Hamel --- tools/doc/addon-verify.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/doc/addon-verify.mjs b/tools/doc/addon-verify.mjs index 670d86b24f3883..4fcf1848d7d342 100644 --- a/tools/doc/addon-verify.mjs +++ b/tools/doc/addon-verify.mjs @@ -14,7 +14,8 @@ // This produces test/addons/01_worker_support/addon.cc. // Sections are numbered in order of first appearance. -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import { open } from 'node:fs/promises'; import { join } from 'node:path'; import { parseArgs } from 'node:util';