Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

18
tools/doc/LICENSE Normal file
View File

@ -0,0 +1,18 @@
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

108
tools/doc/README.md Normal file
View File

@ -0,0 +1,108 @@
Here's how the node docs work.
1:1 relationship from `lib/<module>.js` to `doc/api/<module>.md`.
Each type of heading has a description block.
```markdown
# module
<!--introduced_in=v0.10.0-->
> Stability: 2 - Stable
A description and examples.
## module.property
<!-- YAML
added: v0.10.0
-->
* {type}
A description of the property.
## module.someFunction(x, y, [z=100])
<!-- YAML
added: v0.10.0
-->
* `x` {string} The description of the string.
* `y` {boolean} Should I stay or should I go?
* `z` {number} How many zebras to bring. **Default:** `100`.
A description of the function.
## module.someNewFunction(x)
<!-- YAML
added: REPLACEME
-->
* `x` {string} The description of the string.
This feature is not in a release yet.
## Event: 'blerg'
<!-- YAML
added: v0.10.0
-->
* `anArg` {type} A description of the listener argument.
Modules don't usually raise events on themselves. `cluster` is the
only exception.
## Class: SomeClass
<!-- YAML
added: v0.10.0
-->
A description of the class.
### SomeClass.classMethod(anArg)
<!-- YAML
added: v0.10.0
-->
* `anArg` {Object} Just an argument.
* `field` {string} `anArg` can have this field.
* `field2` {boolean} Another field. **Default:** `false`.
* Returns: {boolean} `true` if it worked.
A description of the method for humans.
### SomeClass.nextSibling()
<!-- YAML
added: v0.10.0
-->
* Returns: {SomeClass | null} The next `SomeClass` in line.
`SomeClass` must be registered in `tools/doc/type-parser.mjs`
to be properly parsed in `{type}` fields.
### SomeClass.someProperty
<!-- YAML
added: v0.10.0
-->
* {string}
The indication of what `someProperty` is.
### Event: 'grelb'
<!-- YAML
added: v0.10.0
-->
* `isBlerg` {boolean}
This event is emitted on instances of `SomeClass`, not on the module itself.
```
* Classes have (description, Properties, Methods, Events).
* Events have (list of listener arguments, description).
* Functions have (list of arguments, returned value if defined, description).
* Methods have (list of arguments, returned value if defined, description).
* Modules have (description, Properties, Functions, Classes, Examples).
* Properties have (type, description).

View File

@ -0,0 +1,93 @@
// doc/api/addons.md has a bunch of code. Extract it for verification
// that the C++ code compiles and the js code runs.
// Add .gyp files which will be used to compile the C++ code.
// Modify the require paths in the js code to pull from the build tree.
// Triggered from the build-addons target in the Makefile and vcbuild.bat.
import { mkdir, writeFile } from 'fs/promises';
import gfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import { readSync } from 'to-vfile';
import { unified } from 'unified';
const rootDir = new URL('../../', import.meta.url);
const doc = new URL('./doc/api/addons.md', rootDir);
const verifyDir = new URL('./test/addons/', rootDir);
const file = readSync(doc, 'utf8');
const tree = unified().use(remarkParse).use(gfm).parse(file);
const addons = {};
let id = 0;
let currentHeader;
const validNames = /^\/\/\s+(.*\.(?:cc|h|js))[\r\n]/;
tree.children.forEach((node) => {
if (node.type === 'heading') {
currentHeader = file.value.slice(
node.children[0].position.start.offset,
node.position.end.offset);
addons[currentHeader] = { files: {} };
} else if (node.type === 'code') {
const match = node.value.match(validNames);
if (match !== null) {
addons[currentHeader].files[match[1]] = node.value;
}
}
});
await Promise.all(
Object.keys(addons).flatMap(
(header) => verifyFiles(addons[header].files, header),
));
function verifyFiles(files, blockName) {
const fileNames = Object.keys(files);
// Must have a .cc and a .js to be a valid test.
if (!fileNames.some((name) => name.endsWith('.cc')) ||
!fileNames.some((name) => name.endsWith('.js'))) {
return [];
}
blockName = blockName.toLowerCase().replace(/\s/g, '_').replace(/\W/g, '');
const dir = new URL(
`./${String(++id).padStart(2, '0')}_${blockName}/`,
verifyDir,
);
files = fileNames.map((name) => {
if (name === 'test.js') {
files[name] = `'use strict';
const common = require('../../common');
${files[name].replace(
"'./build/Release/addon'",
// eslint-disable-next-line no-template-curly-in-string
'`./build/${common.buildType}/addon`')}
`;
}
return {
content: files[name],
name,
url: new URL(`./${name}`, dir),
};
});
files.push({
url: new URL('./binding.gyp', dir),
content: JSON.stringify({
targets: [
{
target_name: 'addon',
sources: files.map(({ name }) => name),
includes: ['../common.gypi'],
},
],
}),
});
const dirCreation = mkdir(dir);
return files.map(({ url, content }) =>
dirCreation.then(() => writeFile(url, content)));
}

116
tools/doc/allhtml.mjs Normal file
View File

@ -0,0 +1,116 @@
// Build all.html by combining the generated toc and apicontent from each
// of the generated html files.
import fs from 'fs';
import buildCSSForFlavoredJS from './buildCSSForFlavoredJS.mjs';
const source = new URL('../../out/doc/api/', import.meta.url);
// Get a list of generated API documents.
const htmlFiles = fs.readdirSync(source, 'utf8')
.filter((name) => name.includes('.html') && name !== 'all.html');
// Read the table of contents.
const toc = fs.readFileSync(new URL('./index.html', source), 'utf8');
// Extract (and concatenate) the toc and apicontent from each document.
let contents = '';
let apicontent = '';
// Identify files that should be skipped. As files are processed, they
// are added to this list to prevent dupes.
const seen = new Set(['all.html', 'index.html']);
for (const link of toc.match(/<a.*?>/g)) {
const href = /href="(.*?)"/.exec(link)[1];
if (!htmlFiles.includes(href) || seen.has(href)) continue;
const data = fs.readFileSync(new URL(`./${href}`, source), 'utf8');
// Split the doc.
const match = /(<\/ul>\s*)?<\/\w+>\s*<\w+ role="main" id="apicontent">/.exec(data);
// Get module name
const moduleName = href.replace(/\.html$/, '');
contents += data.slice(0, match.index)
.replace(/[\s\S]*?id="toc"[^>]*>\s*<\w+>.*?<\/\w+>\s*(<ul>\s*)?/, '')
// Prefix TOC links with current module name
.replace(/<a href="#(?!DEP[0-9]{4})([^"]+)"/g, (match, anchor) => {
return `<a href="#all_${moduleName}_${anchor}"`;
});
apicontent += '<section>' + data.slice(match.index + match[0].length)
.replace(/<!-- API END -->[\s\S]*/, '</section>')
// Prefix all in-page anchor marks with module name
.replace(/<a class="mark" href="#([^"]+)" id="([^"]+)"/g, (match, anchor, id) => {
if (anchor !== id) throw new Error(`Mark does not match: ${anchor} should match ${id}`);
return `<a class="mark" href="#all_${moduleName}_${anchor}" id="all_${moduleName}_${anchor}"`;
})
// Prefix all in-page links with current module name
.replace(/<a href="#(?!DEP[0-9]{4})([^"]+)"/g, (match, anchor) => {
return `<a href="#all_${moduleName}_${anchor}"`;
})
// Update footnote id attributes on anchors
.replace(/<a href="([^"]+)" id="(user-content-fn[^"]+)"/g, (match, href, id) => {
return `<a href="${href}" id="all_${moduleName}_${id}"`;
})
// Update footnote id attributes on list items
.replace(/<(\S+) id="(user-content-fn[^"]+)"/g, (match, tagName, id) => {
return `<${tagName} id="all_${moduleName}_${id}"`;
})
// Prefix all links to other docs modules with those module names
.replace(/<a href="((\w[^#"]*)\.html)#/g, (match, href, linkModule) => {
if (!htmlFiles.includes(href)) return match;
return `<a href="#all_${linkModule}_`;
})
.trim() + '\n';
// Mark source as seen.
seen.add(href);
}
// Replace various mentions of index with all.
let all = toc.replace(/index\.html/g, 'all.html')
.replace('<a href="all.html">', '<a href="index.html">')
.replace('index.json', 'all.json')
.replace('api-section-index', 'api-section-all')
.replace('data-id="index"', 'data-id="all"')
.replace(/<li class="edit_on_github">.*?<\/li>/, '');
// Clean up the title.
all = all.replace(/<title>.*?\| /, '<title>');
// Insert the combined table of contents.
const tocStart = /<!-- TOC -->/.exec(all);
all = all.slice(0, tocStart.index + tocStart[0].length) +
'<details id="toc" open><summary>Table of contents</summary>\n' +
'<ul>\n' + contents + '</ul>\n' +
'</details>\n' +
all.slice(tocStart.index + tocStart[0].length);
// Replace apicontent with the concatenated set of apicontents from each source.
const apiStart = /<\w+ role="main" id="apicontent">\s*/.exec(all);
const apiEnd = all.lastIndexOf('<!-- API END -->');
all = all.slice(0, apiStart.index + apiStart[0].length)
.replace(
'\n</head>',
buildCSSForFlavoredJS(new Set(Array.from(
apicontent.matchAll(/(?<=<pre class="with-)\d+(?=-chars">)/g),
(x) => Number(x[0]),
))) + '\n</head>',
) +
apicontent +
all.slice(apiEnd);
// Write results.
fs.writeFileSync(new URL('./all.html', source), all, 'utf8');
// Validate all hrefs have a target.
const idRe = / id="([^"]+)"/g;
const ids = new Set([...all.matchAll(idRe)].map((match) => match[1]));
const hrefRe = / href="#([^"]+)"/g;
const hrefMatches = all.matchAll(hrefRe);
for (const match of hrefMatches) {
if (!ids.has(match[1])) throw new Error(`link not found: ${match[1]}`);
}

54
tools/doc/alljson.mjs Normal file
View File

@ -0,0 +1,54 @@
// Build all.json by combining the miscs, modules, classes, globals, and methods
// from the generated json files.
import fs from 'fs';
const source = new URL('../../out/doc/api/', import.meta.url);
// Get a list of generated API documents.
const jsonFiles = fs.readdirSync(source, 'utf8')
.filter((name) => name.includes('.json') && name !== 'all.json');
// Read the table of contents.
const toc = fs.readFileSync(new URL('./index.html', source), 'utf8');
// Initialize results. Only these four data values will be collected.
const results = {
miscs: [],
modules: [],
classes: [],
globals: [],
methods: [],
};
// Identify files that should be skipped. As files are processed, they
// are added to this list to prevent dupes.
const seen = new Set(['all.json', 'index.json']);
// Extract (and concatenate) the selected data from each document.
// Expand hrefs found in json to include source HTML file.
for (const link of toc.match(/<a.*?>/g)) {
const href = /href="(.*?)"/.exec(link)[1];
const json = href.replace('.html', '.json');
if (!jsonFiles.includes(json) || seen.has(json)) continue;
const data = JSON.parse(
fs.readFileSync(new URL(`./${json}`, source), 'utf8')
.replace(/<a href=\\"#/g, `<a href=\\"${href}#`),
);
for (const property in data) {
if (Object.hasOwn(results, property)) {
data[property].forEach((mod) => {
mod.source = data.source;
});
results[property].push(...data[property]);
}
}
// Mark source as seen.
seen.add(json);
}
// Write results.
fs.writeFileSync(new URL('./all.json', source),
`${JSON.stringify(results, null, 2)}\n`, 'utf8');

212
tools/doc/apilinks.mjs Normal file
View File

@ -0,0 +1,212 @@
// Scan API sources for definitions.
//
// Note the output is produced based on a world class parser, adherence to
// conventions, and a bit of guess work. Examples:
//
// * We scan for top level module.exports statements, and determine what
// is exported by looking at the source code only (i.e., we don't do
// an eval). If exports include `Foo`, it probably is a class, whereas
// if what is exported is `constants` it probably is prefixed by the
// basename of the source file (e.g., `zlib`), unless that source file is
// `buffer.js`, in which case the name is just `buf`. unless the constant
// is `kMaxLength`, in which case it is `buffer`.
//
// * We scan for top level definitions for those exports, handling
// most common cases (e.g., `X.prototype.foo =`, `X.foo =`,
// `function X(...) {...}`). Over time, we expect to handle more
// cases (example: ES2015 class definitions).
import child_process from 'child_process';
import fs from 'fs';
import path from 'path';
import * as acorn from '../../deps/acorn/acorn/dist/acorn.mjs';
// Run a command, capturing stdout, ignoring errors.
function execSync(command) {
try {
return child_process.execSync(
command,
{ stdio: ['ignore', null, 'ignore'] },
).toString().trim();
} catch {
return '';
}
}
// Determine origin repo and tag (or hash) of the most recent commit.
const localBranch = execSync('git name-rev --name-only HEAD');
const trackingRemote = execSync(`git config branch.${localBranch}.remote`);
const remoteUrl = execSync(`git config remote.${trackingRemote}.url`);
const repo = (remoteUrl.match(/(\w+\/\w+)\.git\r?\n?$/) ||
['', 'nodejs/node'])[1];
const hash = execSync('git log -1 --pretty=%H') || 'main';
const tag = execSync(`git describe --contains ${hash}`).split('\n')[0] || hash;
// Extract definitions from each file specified.
const definition = {};
const output = process.argv[2];
const inputs = process.argv.slice(3);
inputs.forEach((file) => {
const basename = path.basename(file, '.js');
// Parse source.
const source = fs.readFileSync(file, 'utf8');
const ast = acorn.parse(source, {
allowReturnOutsideFunction: true,
ecmaVersion: 'latest',
locations: true,
});
const program = ast.body;
// Build link
const link =
`https://github.com/${repo}/blob/${tag}/${path.relative('.', file).replace(/\\/g, '/')}`;
// Scan for exports.
const exported = { constructors: [], identifiers: [] };
const indirect = {};
program.forEach((statement) => {
if (statement.type === 'ExpressionStatement') {
const expr = statement.expression;
if (expr.type !== 'AssignmentExpression') return;
let lhs = expr.left;
if (lhs.type !== 'MemberExpression') return;
if (lhs.object.type === 'MemberExpression') lhs = lhs.object;
if (lhs.object.name === 'exports') {
const name = lhs.property.name;
if (expr.right.type === 'FunctionExpression') {
definition[`${basename}.${name}`] =
`${link}#L${statement.loc.start.line}`;
} else if (expr.right.type === 'Identifier') {
if (expr.right.name === name) {
indirect[name] = `${basename}.${name}`;
}
} else {
exported.identifiers.push(name);
}
} else if (lhs.object.name === 'module') {
if (lhs.property.name !== 'exports') return;
let rhs = expr.right;
while (rhs.type === 'AssignmentExpression') rhs = rhs.right;
if (rhs.type === 'NewExpression') {
exported.constructors.push(rhs.callee.name);
} else if (rhs.type === 'ObjectExpression') {
rhs.properties.forEach((property) => {
if (property.value.type === 'Identifier') {
exported.identifiers.push(property.value.name);
if (/^[A-Z]/.test(property.value.name[0])) {
exported.constructors.push(property.value.name);
}
}
});
} else if (rhs.type === 'Identifier') {
exported.identifiers.push(rhs.name);
}
}
} else if (statement.type === 'VariableDeclaration') {
for (const decl of statement.declarations) {
let init = decl.init;
while (init && init.type === 'AssignmentExpression') init = init.left;
if (!init || init.type !== 'MemberExpression') continue;
if (init.object.name === 'exports') {
definition[`${basename}.${init.property.name}`] =
`${link}#L${statement.loc.start.line}`;
} else if (init.object.name === 'module') {
if (init.property.name !== 'exports') continue;
exported.constructors.push(decl.id.name);
definition[decl.id.name] = `${link}#L${statement.loc.start.line}`;
}
}
}
});
// Scan for definitions matching those exports; currently supports:
//
// ClassName.foo = ...;
// ClassName.prototype.foo = ...;
// function Identifier(...) {...};
// class Foo {...};
//
program.forEach((statement) => {
if (statement.type === 'ExpressionStatement') {
const expr = statement.expression;
if (expr.type !== 'AssignmentExpression') return;
if (expr.left.type !== 'MemberExpression') return;
let object;
if (expr.left.object.type === 'MemberExpression') {
if (expr.left.object.property.name !== 'prototype') return;
object = expr.left.object.object;
} else if (expr.left.object.type === 'Identifier') {
object = expr.left.object;
} else {
return;
}
if (!exported.constructors.includes(object.name)) return;
let objectName = object.name;
if (expr.left.object.type === 'MemberExpression') {
objectName = objectName.toLowerCase();
if (objectName === 'buffer') objectName = 'buf';
}
let name = expr.left.property.name;
if (expr.left.computed) {
name = `${objectName}[${name}]`;
} else {
name = `${objectName}.${name}`;
}
definition[name] = `${link}#L${statement.loc.start.line}`;
if (expr.left.property.name === expr.right.name) {
indirect[expr.right.name] = name;
}
} else if (statement.type === 'FunctionDeclaration') {
const name = statement.id.name;
if (!exported.identifiers.includes(name)) return;
if (basename.startsWith('_')) return;
definition[`${basename}.${name}`] =
`${link}#L${statement.loc.start.line}`;
} else if (statement.type === 'ClassDeclaration') {
if (!exported.constructors.includes(statement.id.name)) return;
definition[statement.id.name] = `${link}#L${statement.loc.start.line}`;
const name = statement.id.name.slice(0, 1).toLowerCase() +
statement.id.name.slice(1);
statement.body.body.forEach((defn) => {
if (defn.type !== 'MethodDefinition') return;
if (defn.kind === 'method') {
definition[`${name}.${defn.key.name}`] =
`${link}#L${defn.loc.start.line}`;
} else if (defn.kind === 'constructor') {
definition[`new ${statement.id.name}`] =
`${link}#L${defn.loc.start.line}`;
}
});
}
});
// Search for indirect references of the form ClassName.foo = foo;
if (Object.keys(indirect).length > 0) {
program.forEach((statement) => {
if (statement.type === 'FunctionDeclaration') {
const name = statement.id.name;
if (indirect[name]) {
definition[indirect[name]] = `${link}#L${statement.loc.start.line}`;
}
}
});
}
});
fs.writeFileSync(output, JSON.stringify(definition, null, 2), 'utf8');

View File

@ -0,0 +1,53 @@
const CHAR_THRESHOLD = 68; // Around 68 characters, we have to take into
// account the left column that appears on screen
// wider than 1024px.
const ESTIMATED_CHAR_WIDTH = 8; // If the root element font-size is 16px (default value), 1ch is 7.8025px.
const TOGGLE_WIDTH = 142; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L954
const PRE_MARGIN_LEFT = 16; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L516
const PRE_MARGIN_RIGHT = 16; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L516
const PRE_PADDING_LEFT = 16; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L513
const PRE_PADDING_RIGHT = 16; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L513
const COLUMN_RIGHT_MARGIN_LEFT_LARGE_SCREEN = 234; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L653
const COLUMN_RIGHT_MARGIN_LEFT_SMALL_SCREEN = 0; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L906
const COLUMN_RIGHT_MARGIN_RIGHT_LARGE_SCREEN = 0;
const COLUMN_RIGHT_MARGIN_RIGHT_SMALL_SCREEN = 0;
const COLUMN_RIGHT_PADDING_LEFT_LARGE_SCREEN = 24; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L655
const COLUMN_RIGHT_PADDING_LEFT_SMALL_SCREEN = 8; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L907
const COLUMN_RIGHT_PADDING_RIGHT_LARGE_SCREEN = 32; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L654
const COLUMN_RIGHT_PADDING_RIGHT_SMALL_SCREEN = 8; // https://github.com/nodejs/node/blob/dbd938549b16718f2e4cc2809746b8c44a191b1d/doc/api_assets/style.css#L908
const getMarginLeft = (charCount) =>
(charCount > CHAR_THRESHOLD ?
COLUMN_RIGHT_MARGIN_LEFT_LARGE_SCREEN :
COLUMN_RIGHT_MARGIN_LEFT_SMALL_SCREEN) + PRE_MARGIN_LEFT;
const getPaddingLeft = (charCount) =>
(charCount > CHAR_THRESHOLD ?
COLUMN_RIGHT_PADDING_LEFT_LARGE_SCREEN :
COLUMN_RIGHT_PADDING_LEFT_SMALL_SCREEN) + PRE_PADDING_LEFT;
const getPaddingRight = (charCount) =>
(charCount > CHAR_THRESHOLD ?
COLUMN_RIGHT_PADDING_RIGHT_LARGE_SCREEN :
COLUMN_RIGHT_PADDING_RIGHT_SMALL_SCREEN) + PRE_PADDING_RIGHT;
const getMarginRight = (charCount) =>
(charCount > CHAR_THRESHOLD ?
COLUMN_RIGHT_MARGIN_RIGHT_LARGE_SCREEN :
COLUMN_RIGHT_MARGIN_RIGHT_SMALL_SCREEN) + PRE_MARGIN_RIGHT;
export default function buildCSSForFlavoredJS(dynamicSizes) {
if (dynamicSizes == null || dynamicSizes.length === 0) return '';
return `<style>${Array.from(dynamicSizes, (charCount) =>
`@media(max-width:${getMarginLeft(charCount) + getPaddingLeft(charCount) +
charCount * ESTIMATED_CHAR_WIDTH + TOGGLE_WIDTH +
getPaddingRight(charCount) + getMarginRight(charCount)}px){` +
`.with-${charCount}-chars>.js-flavor-toggle{` +
'float:none;' +
'margin:0 0 1em auto;' +
'}' +
'}').join('')}</style>`;
}

37
tools/doc/common.mjs Normal file
View File

@ -0,0 +1,37 @@
import yaml from 'js-yaml';
export function isYAMLBlock(text) {
return /^<!-- YAML/.test(text);
}
export function isSourceLink(text) {
return /^<!-- source_link=([^\s/]+\/)+\w+\.\w+ -->/.test(text);
}
export function arrify(value) {
return Array.isArray(value) ? value : [value];
}
export function extractAndParseYAML(text) {
text = text.trim()
.replace(/^<!-- YAML/, '')
.replace(/-->$/, '');
// js-yaml.load() throws on error.
const meta = yaml.load(text);
// Since semver-minors can trickle down to previous major versions,
// features may have been added in multiple versions.
meta.added &&= arrify(meta.added);
meta.napiVersion &&= arrify(meta.napiVersion);
// Treat deprecated like added for consistency.
meta.deprecated &&= arrify(meta.deprecated);
meta.removed &&= arrify(meta.removed);
meta.changes ||= [];
return meta;
}

View File

@ -0,0 +1,92 @@
import fs from 'fs';
import { resolve } from 'path';
import assert from 'assert';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
const source = resolve(process.argv[2]);
const skipDeprecationComment = /^<!-- md-lint skip-deprecation (DEP\d{4}) -->$/;
const generateDeprecationCode = (codeAsNumber) =>
`DEP${codeAsNumber.toString().padStart(4, '0')}`;
const addMarkdownPathToErrorStack = (error, node) => {
const { line, column } = node.position.start;
const [header, ...lines] = error.stack.split('\n');
error.stack =
header +
`\n at <anonymous> (${source}:${line}:${column})\n` +
lines.join('\n');
return error;
};
const testHeading = (headingNode, expectedDeprecationCode) => {
try {
assert.strictEqual(
headingNode?.children[0]?.value.substring(0, 9),
`${expectedDeprecationCode}: `,
'Ill-formed or out-of-order deprecation code.',
);
} catch (e) {
throw addMarkdownPathToErrorStack(e, headingNode);
}
};
const testYAMLComment = (commentNode) => {
try {
assert.match(
commentNode?.value?.substring(0, 21),
/^<!-- YAML\r?\nchanges:\r?\n/,
'Missing or ill-formed YAML comment.',
);
} catch (e) {
throw addMarkdownPathToErrorStack(e, commentNode);
}
};
const testDeprecationType = (paragraphNode) => {
try {
assert.strictEqual(
paragraphNode?.children[0]?.value?.substring(0, 6),
'Type: ',
'Missing deprecation type.',
);
} catch (e) {
throw addMarkdownPathToErrorStack(e, paragraphNode);
}
};
const tree = unified()
.use(remarkParse)
.parse(fs.readFileSync(source));
let expectedDeprecationCodeNumber = 0;
for (let i = 0; i < tree.children.length; i++) {
const node = tree.children[i];
if (node.type === 'html' && skipDeprecationComment.test(node.value)) {
const expectedDeprecationCode =
generateDeprecationCode(++expectedDeprecationCodeNumber);
const deprecationCodeAsText = node.value.match(skipDeprecationComment)[1];
try {
assert.strictEqual(
deprecationCodeAsText,
expectedDeprecationCode,
'Deprecation codes are not ordered correctly.',
);
} catch (e) {
throw addMarkdownPathToErrorStack(e, node);
}
}
if (node.type === 'heading' && node.depth === 3) {
const expectedDeprecationCode =
generateDeprecationCode(++expectedDeprecationCodeNumber);
testHeading(node, expectedDeprecationCode);
testYAMLComment(tree.children[i + 1]);
testDeprecationType(tree.children[i + 2]);
}
}

7
tools/doc/deps.mjs Normal file
View File

@ -0,0 +1,7 @@
// Re-exporting dependencies for tests in test/doctool.
export { default as rehypeRaw } from 'rehype-raw';
export { default as rehypeStringify } from 'rehype-stringify';
export { default as remarkParse } from 'remark-parse';
export { default as remarkRehype } from 'remark-rehype';
export { unified } from 'unified';

View File

@ -0,0 +1,7 @@
// Flags: --expose-internals
import internal from 'internal/options';
import { writeFileSync } from 'fs';
const schema = internal.generateConfigJsonSchema();
writeFileSync('doc/node-config-schema.json', `${JSON.stringify(schema, null, 2)}\n`);

135
tools/doc/generate.mjs Normal file
View File

@ -0,0 +1,135 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
import { readFileSync, promises as fs } from 'fs';
import path from 'path';
import raw from 'rehype-raw';
import htmlStringify from 'rehype-stringify';
import gfm from 'remark-gfm';
import markdown from 'remark-parse';
import remark2rehype from 'remark-rehype';
import frontmatter from 'remark-frontmatter';
import { unified } from 'unified';
import * as html from './html.mjs';
import * as json from './json.mjs';
import { replaceLinks } from './markdown.mjs';
const linksMapperFile = new URL('links-mapper.json', import.meta.url);
const linksMapper = JSON.parse(readFileSync(linksMapperFile, 'utf8'));
// Parse the args.
// Don't use nopt or whatever for this. It's simple enough.
const args = process.argv.slice(2);
let filename = null;
let nodeVersion = null;
let outputDir = null;
let apilinks = {};
let versions = [];
async function main() {
for (const arg of args) {
if (!arg.startsWith('--')) {
filename = arg;
} else if (arg.startsWith('--node-version=')) {
nodeVersion = arg.replace(/^--node-version=/, '');
} else if (arg.startsWith('--output-directory=')) {
outputDir = arg.replace(/^--output-directory=/, '');
} else if (arg.startsWith('--apilinks=')) {
const linkFile = arg.replace(/^--apilinks=/, '');
const data = await fs.readFile(linkFile, 'utf8');
if (!data.trim()) {
throw new Error(`${linkFile} is empty`);
}
apilinks = JSON.parse(data);
} else if (arg.startsWith('--versions-file=')) {
const versionsFile = arg.replace(/^--versions-file=/, '');
const data = await fs.readFile(versionsFile, 'utf8');
if (!data.trim()) {
throw new Error(`${versionsFile} is empty`);
}
versions = JSON.parse(data);
}
}
nodeVersion ||= process.version;
if (!filename) {
throw new Error('No input file specified');
} else if (!outputDir) {
throw new Error('No output directory specified');
}
const input = await fs.readFile(filename, 'utf8');
const content = await unified()
.use(frontmatter)
.use(replaceLinks, { filename, linksMapper })
.use(markdown)
.use(gfm)
.use(html.preprocessText, { nodeVersion })
.use(json.jsonAPI, { filename })
.use(html.firstHeader)
.use(html.preprocessElements, { filename })
.use(html.buildToc, { filename, apilinks })
.use(remark2rehype, { allowDangerousHtml: true })
.use(raw)
.use(htmlStringify)
.process(input);
const myHtml = await html.toHTML({ input, content, filename, nodeVersion,
versions });
const basename = path.basename(filename, '.md');
const htmlTarget = path.join(outputDir, `${basename}.html`);
const jsonTarget = path.join(outputDir, `${basename}.json`);
return Promise.allSettled([
fs.writeFile(htmlTarget, myHtml),
fs.writeFile(jsonTarget, JSON.stringify(content.json, null, 2)),
]);
}
main()
.then((tasks) => {
// Filter rejected tasks
const errors = tasks.filter(({ status }) => status === 'rejected')
.map(({ reason }) => reason);
// Log errors
for (const error of errors) {
console.error(error);
}
// Exit process with code 1 if some errors
if (errors.length > 0) {
return process.exit(1);
}
// Else with code 0
process.exit(0);
})
.catch((error) => {
console.error(error);
process.exit(1);
});

612
tools/doc/html.mjs Normal file
View File

@ -0,0 +1,612 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
import fs from 'fs';
import path from 'path';
import highlightJs from 'highlight.js';
import raw from 'rehype-raw';
import htmlStringify from 'rehype-stringify';
import gfm from 'remark-gfm';
import markdown from 'remark-parse';
import remark2rehype from 'remark-rehype';
import { unified } from 'unified';
import { visit } from 'unist-util-visit';
import * as common from './common.mjs';
import * as typeParser from './type-parser.mjs';
import buildCSSForFlavoredJS from './buildCSSForFlavoredJS.mjs';
const dynamicSizes = { __proto__: null };
const { highlight, getLanguage } = highlightJs;
const docPath = new URL('../../doc/', import.meta.url);
// Add class attributes to index navigation links.
function navClasses() {
return (tree) => {
visit(tree, { type: 'element', tagName: 'a' }, (node) => {
node.properties.class = 'nav-' +
node.properties.href.replace('.html', '').replace(/\W+/g, '-');
});
};
}
const gtocPath = new URL('./api/index.md', docPath);
const gtocMD = fs.readFileSync(gtocPath, 'utf8')
.replace(/\(([^#?]+?)\.md\)/ig, (_, filename) => `(${filename}.html)`)
.replace(/^<!--.*?-->/gms, '');
const gtocHTML = unified()
.use(markdown)
.use(gfm)
.use(remark2rehype, { allowDangerousHtml: true })
.use(raw)
.use(navClasses)
.use(htmlStringify)
.processSync(gtocMD).toString();
const templatePath = new URL('./template.html', docPath);
const template = fs.readFileSync(templatePath, 'utf8');
function processContent(content) {
content = content.toString();
// Increment header tag levels to avoid multiple h1 tags in a doc.
// This means we can't already have an <h6>.
if (content.includes('<h6>')) {
throw new Error('Cannot increment a level 6 header');
}
// `++level` to convert the string to a number and increment it.
content = content.replace(/(?<=<\/?h)[1-5](?=[^<>]*>)/g, (level) => ++level);
// Wrap h3 tags in section tags unless they are immediately preceded by a
// section tag. The latter happens when GFM footnotes are generated. We don't
// want to add another section tag to the footnotes section at the end of the
// document because that will result in an empty section element. While not an
// HTML error, it's enough for validator.w3.org to print a warning.
let firstTime = true;
return content
.replace(/(?<!<section [^>]+>)<h3/g, (heading) => {
if (firstTime) {
firstTime = false;
return '<section>' + heading;
}
return '</section><section>' + heading;
}) + (firstTime ? '' : '</section>');
}
export function toHTML({ input, content, filename, nodeVersion, versions }) {
const dynamicSizesForThisFile = dynamicSizes[filename];
filename = path.basename(filename, '.md');
const id = filename.replace(/\W+/g, '-');
let HTML = template.replace('__ID__', id)
.replace(/__FILENAME__/g, filename)
.replace('__SECTION__', content.section)
.replace(/__VERSION__/g, nodeVersion)
.replace(/__TOC__/g, content.toc)
.replace('__JS_FLAVORED_DYNAMIC_CSS__', buildCSSForFlavoredJS(dynamicSizesForThisFile))
.replace(/__TOC_PICKER__/g, tocPicker(id, content))
.replace(/__GTOC_PICKER__/g, gtocPicker(id))
.replace(/__GTOC__/g, gtocHTML.replace(
`class="nav-${id}"`, `class="nav-${id} active"`))
.replace('__EDIT_ON_GITHUB__', editOnGitHub(filename))
.replace('__CONTENT__', processContent(content));
const docCreated = input.match(
/<!--\s*introduced_in\s*=\s*v([0-9]+)\.([0-9]+)\.[0-9]+\s*-->/);
if (docCreated) {
HTML = HTML.replace('__ALTDOCS__', altDocs(filename, docCreated, versions));
} else {
console.error(`Failed to add alternative version links to ${filename}`);
HTML = HTML.replace('__ALTDOCS__', '');
}
return HTML;
}
// Set the section name based on the first header. Default to 'Index'.
export function firstHeader() {
return (tree, file) => {
let heading;
visit(tree, (node) => {
if (node.type === 'heading') {
heading = node;
return false;
}
});
if (heading && heading.children.length) {
const recursiveTextContent = (node) =>
node.value || node.children.map(recursiveTextContent).join('');
file.section = recursiveTextContent(heading);
} else {
file.section = 'Index';
}
};
}
// Handle general body-text replacements.
// For example, link man page references to the actual page.
export function preprocessText({ nodeVersion }) {
return (tree) => {
visit(tree, null, (node) => {
if (common.isSourceLink(node.value)) {
const [path] = node.value.match(/(?<=<!-- source_link=).*(?= -->)/);
node.value = `<p><strong>Source Code:</strong> <a href="https://github.com/nodejs/node/blob/${nodeVersion}/${path}">${path}</a></p>`;
} else if (node.type === 'text' && node.value) {
const value = linkJsTypeDocs(linkManPages(node.value));
if (value !== node.value) {
node.type = 'html';
node.value = value;
}
}
});
};
}
// Syscalls which appear in the docs, but which only exist in BSD / macOS.
const BSD_ONLY_SYSCALLS = new Set(['lchmod']);
const MAN_PAGE = /(^|\s)([a-z.]+)\((\d)([a-z]?)\)/gm;
// Handle references to man pages, eg "open(2)" or "lchmod(2)".
// Returns modified text, with such refs replaced with HTML links, for example
// '<a href="http://man7.org/linux/man-pages/man2/open.2.html">open(2)</a>'.
function linkManPages(text) {
return text.replace(
MAN_PAGE, (match, beginning, name, number, optionalCharacter) => {
// Name consists of lowercase letters,
// number is a single digit with an optional lowercase letter.
const displayAs = `<code>${name}(${number}${optionalCharacter})</code>`;
if (BSD_ONLY_SYSCALLS.has(name)) {
return `${beginning}<a href="https://www.freebsd.org/cgi/man.cgi?query=${name}&sektion=${number}">${displayAs}</a>`;
}
return `${beginning}<a href="http://man7.org/linux/man-pages/man${number}/${name}.${number}${optionalCharacter}.html">${displayAs}</a>`;
});
}
const TYPE_SIGNATURE = /\{[^}]+\}/g;
function linkJsTypeDocs(text) {
const parts = text.split('`');
// Handle types, for example the source Markdown might say
// "This argument should be a {number} or {string}".
for (let i = 0; i < parts.length; i += 2) {
const typeMatches = parts[i].match(TYPE_SIGNATURE);
if (typeMatches) {
typeMatches.forEach((typeMatch) => {
parts[i] = parts[i].replace(typeMatch, typeParser.toLink(typeMatch));
});
}
}
return parts.join('`');
}
const isJSFlavorSnippet = (node) => node.lang === 'cjs' || node.lang === 'mjs';
// Preprocess headers, stability blockquotes, and YAML blocks.
export function preprocessElements({ filename }) {
return (tree) => {
const STABILITY_RE = /(.*:)\s*(\d)([\s\S]*)/;
let headingIndex = -1;
let heading = null;
visit(tree, null, (node, index, parent) => {
if (node.type === 'heading') {
headingIndex = index;
if (heading) {
node.parentHeading = heading;
for (let d = heading.depth; d >= node.depth; d--) {
node.parentHeading = node.parentHeading.parentHeading;
}
if (heading.depth > 2 || node.depth > 2) {
const isNonWrapped = node.depth > 2; // For depth of 1 and 2, there's already a wrapper.
parent.children.splice(index++, 0, {
type: 'html',
value:
`</div>`.repeat(heading.depth - node.depth + isNonWrapped) +
(isNonWrapped ? '<div>' : ''),
});
}
}
heading = node;
} else if (node.type === 'code') {
if (!node.lang) {
console.warn(
`No language set in ${filename}, line ${node.position.start.line}`,
);
}
const className = isJSFlavorSnippet(node) ?
`language-js ${node.lang}` :
`language-${node.lang}`;
const highlighted =
`<code class='${className}'>${(getLanguage(node.lang || '') ? highlight(node.value, { language: node.lang }) : node).value}</code>`;
node.type = 'html';
const copyButton = '<button class="copy-button">copy</button>';
if (isJSFlavorSnippet(node)) {
const previousNode = parent.children[index - 1] || {};
const nextNode = parent.children[index + 1] || {};
const charCountFirstTwoLines = Math.max(...node.value.split('\n', 2).map((str) => str.length));
if (!isJSFlavorSnippet(previousNode) &&
isJSFlavorSnippet(nextNode) &&
nextNode.lang !== node.lang) {
// Saving the highlight code as value to be added in the next node.
node.value = highlighted;
node.charCountFirstTwoLines = charCountFirstTwoLines;
} else if (isJSFlavorSnippet(previousNode) &&
previousNode.lang !== node.lang) {
const actualCharCount = Math.max(charCountFirstTwoLines, previousNode.charCountFirstTwoLines);
(dynamicSizes[filename] ??= new Set()).add(actualCharCount);
node.value = `<pre class="with-${actualCharCount}-chars">` +
'<input class="js-flavor-toggle" type="checkbox"' +
// If CJS comes in second, ESM should display by default.
(node.lang === 'cjs' ? ' checked' : '') +
' aria-label="Show modern ES modules syntax">' +
previousNode.value +
highlighted +
copyButton +
'</pre>';
node.lang = null;
previousNode.value = '';
previousNode.lang = null;
} else {
// Isolated JS snippet, no need to add the checkbox.
node.value = `<pre>${highlighted} ${copyButton}</pre>`;
}
} else {
node.value = `<pre>${highlighted} ${copyButton}</pre>`;
}
} else if (node.type === 'html' && common.isYAMLBlock(node.value)) {
node.value = parseYAML(node.value);
} else if (node.type === 'blockquote') {
const paragraph = node.children[0].type === 'paragraph' &&
node.children[0];
const text = paragraph && paragraph.children[0].type === 'text' &&
paragraph.children[0];
if (text && text.value.includes('Stability:')) {
const [, prefix, number, explication] =
text.value.match(STABILITY_RE);
// Stability indices are never more than 3 nodes away from their
// heading.
const isStabilityIndex = index - headingIndex <= 3;
if (heading && isStabilityIndex) {
heading.stability = number;
for (let h = heading; h != null; h = h.parentHeading) {
if (!h.hasStabilityIndexElement) continue;
if (h.stability === number && h.explication === explication) {
throw new Error(`Duplicate stability index at ${filename}:${node.position.start.line}, it already inherits it from a parent heading ${filename}:${h.position.start.line}`);
} else break;
}
heading.hasStabilityIndexElement = true;
heading.explication = explication;
headingIndex = -1;
}
// Do not link to the section we are already in.
const noLinking = filename.includes('documentation') &&
!heading.hasStabilityIndexElement && heading.children[0].value === 'Stability index';
// Collapse blockquote and paragraph into a single node
node.type = 'paragraph';
node.children.shift();
node.children.unshift(...paragraph.children);
// Insert div with prefix and number
node.children.unshift({
type: 'html',
value: `<div class="api_stability api_stability_${number}">` +
(noLinking ? '' :
'<a href="documentation.html#stability-index">') +
`${prefix} ${number}${noLinking ? '' : '</a>'}`
.replace(/\n/g, ' '),
});
// Remove prefix and number from text
text.value = explication;
// close div
node.children.push({ type: 'html', value: '</div>' });
}
}
// In case we've inserted/removed node(s) before the current one, we need
// to make sure we're not visiting the same node again or skipping one.
return [true, index + 1];
});
};
}
function parseYAML(text) {
const meta = common.extractAndParseYAML(text);
let result = '<div class="api_metadata">\n';
const added = { description: '' };
const deprecated = { description: '' };
const removed = { description: '' };
if (meta.added) {
added.version = meta.added;
added.description = `<span>Added in: ${added.version.join(', ')}</span>`;
}
if (meta.deprecated) {
deprecated.version = meta.deprecated;
deprecated.description =
`<span>Deprecated since: ${deprecated.version.join(', ')}</span>`;
}
if (meta.removed) {
removed.version = meta.removed;
removed.description = `<span>Removed in: ${removed.version.join(', ')}</span>`;
}
if (meta.changes.length > 0) {
if (deprecated.description) meta.changes.push(deprecated);
if (removed.description) meta.changes.push(removed);
meta.changes.sort((a, b) => versionSort(a.version, b.version));
if (added.description) meta.changes.push(added);
result += '<details class="changelog"><summary>History</summary>\n' +
'<table>\n<tr><th>Version</th><th>Changes</th></tr>\n';
meta.changes.forEach((change) => {
const description = unified()
.use(markdown)
.use(gfm)
.use(remark2rehype, { allowDangerousHtml: true })
.use(raw)
.use(htmlStringify)
.processSync(change.description).toString();
const version = common.arrify(change.version).join(', ');
result += `<tr><td>${version}</td>\n` +
`<td>${description}</td></tr>\n`;
});
result += '</table>\n</details>\n';
} else {
result += `${added.description}${deprecated.description}${removed.description}\n`;
}
if (meta.napiVersion) {
result += `<span>N-API version: ${meta.napiVersion.join(', ')}</span>\n`;
}
result += '</div>';
return result;
}
function minVersion(a) {
return common.arrify(a).reduce((min, e) => {
return !min || versionSort(min, e) < 0 ? e : min;
});
}
const numberRe = /^\d*/;
function versionSort(a, b) {
a = minVersion(a).trim();
b = minVersion(b).trim();
let i = 0; // Common prefix length.
while (i < a.length && i < b.length && a[i] === b[i]) i++;
a = a.substring(i);
b = b.substring(i);
return +b.match(numberRe)[0] - +a.match(numberRe)[0];
}
const DEPRECATION_HEADING_PATTERN = /^DEP\d+:/;
export function buildToc({ filename, apilinks }) {
return (tree, file) => {
const idCounters = { __proto__: null };
const legacyIdCounters = { __proto__: null };
let toc = '';
let depth = 0;
visit(tree, null, (node) => {
if (node.type !== 'heading') return;
if (node.depth - depth > 1) {
throw new Error(
`Inappropriate heading level:\n${JSON.stringify(node)}`,
);
}
depth = node.depth;
const realFilename = path.basename(filename, '.md');
const headingText = file.value.slice(
node.children[0].position.start.offset,
node.position.end.offset).trim();
const id = getId(headingText, idCounters);
// Use previous ID generator to create alias
const legacyId = getLegacyId(`${realFilename}_${headingText}`, legacyIdCounters);
const isDeprecationHeading =
DEPRECATION_HEADING_PATTERN.test(headingText);
if (isDeprecationHeading) {
node.data ||= {};
node.data.hProperties ||= {};
node.data.hProperties.id =
headingText.substring(0, headingText.indexOf(':'));
}
const hasStability = node.stability !== undefined;
toc += ' '.repeat((depth - 1) * 2) +
(hasStability ? `* <span class="stability_${node.stability}">` : '* ') +
`<a href="#${isDeprecationHeading ? node.data.hProperties.id : id}">${headingText}</a>${hasStability ? '</span>' : ''}\n`;
let anchor =
`<span><a class="mark" href="#${id}" id="${id}">#</a></span>`;
// Add alias anchor to preserve old links
anchor += `<a aria-hidden="true" class="legacy" id="${legacyId}"></a>`;
if (realFilename === 'errors' && headingText.startsWith('ERR_')) {
anchor +=
`<span><a class="mark" href="#${headingText}" id="${headingText}">#</a></span>`;
}
const api = headingText.replace(/^.*:\s+/, '').replace(/\(.*/, '');
if (apilinks[api]) {
anchor = `<a class="srclink" href=${apilinks[api]}>[src]</a>${anchor}`;
}
node.children.push({ type: 'html', value: anchor });
});
if (toc !== '') {
const inner = unified()
.use(markdown)
.use(gfm)
.use(remark2rehype, { allowDangerousHtml: true })
.use(raw)
.use(htmlStringify)
.processSync(toc).toString();
file.toc = `<details role="navigation" id="toc" open><summary>Table of contents</summary>${inner}</details>`;
file.tocPicker = `<div class="toc">${inner}</div>`;
} else {
file.toc = file.tocPicker = '<!-- TOC -->';
}
};
}
// ID generator that mirrors Github's heading anchor parser
const punctuation = /[^\w\- ]/g;
function getId(text, idCounters) {
text = text.toLowerCase()
.replace(punctuation, '')
.replace(/ /g, '-');
if (idCounters[text] !== undefined) {
return `${text}_${++idCounters[text]}`;
}
idCounters[text] = 0;
return text;
}
// This ID generator is purely to generate aliases
// so we can preserve old doc links
const notAlphaNumerics = /[^a-z0-9]+/g;
const edgeUnderscores = /^_+|_+$/g;
const notAlphaStart = /^[^a-z]/;
function getLegacyId(text, idCounters) {
text = text.toLowerCase()
.replace(notAlphaNumerics, '_')
.replace(edgeUnderscores, '')
.replace(notAlphaStart, '_$&');
if (idCounters[text] !== undefined) {
return `${text}_${++idCounters[text]}`;
}
idCounters[text] = 0;
return text;
}
function altDocs(filename, docCreated, versions) {
const [, docCreatedMajor, docCreatedMinor] = docCreated.map(Number);
const host = 'https://nodejs.org';
const getHref = (versionNum) =>
`${host}/docs/latest-v${versionNum}/api/${filename}.html`;
const wrapInListItem = (version) =>
`<li><a href="${getHref(version.num)}">${version.num}${version.lts ? ' <b>LTS</b>' : ''}</a></li>`;
function isDocInVersion(version) {
const [versionMajor, versionMinor] = version.num.split('.').map(Number);
if (docCreatedMajor > versionMajor) return false;
if (docCreatedMajor < versionMajor) return true;
if (Number.isNaN(versionMinor)) return true;
return docCreatedMinor <= versionMinor;
}
const list = versions.filter(isDocInVersion).map(wrapInListItem).join('\n');
return list ? `
<li class="picker-header">
<a href="#alt-docs" aria-controls="alt-docs">
<span class="picker-arrow"></span>
Other versions
</a>
<div class="picker" tabindex="-1"><ol id="alt-docs">${list}</ol></div>
</li>
` : '';
}
function editOnGitHub(filename) {
return `<li class="edit_on_github"><a href="https://github.com/nodejs/node/edit/main/doc/api/${filename}.md">Edit on GitHub</a></li>`;
}
function gtocPicker(id) {
if (id === 'index') {
return '';
}
// Highlight the current module and add a link to the index
const gtoc = gtocHTML.replace(
`class="nav-${id}"`, `class="nav-${id} active"`,
).replace('</ul>', `
<li>
<a href="index.html">Index</a>
</li>
</ul>
`);
return `
<li class="picker-header">
<a href="#gtoc-picker" aria-controls="gtoc-picker">
<span class="picker-arrow"></span>
Index
</a>
<div class="picker" tabindex="-1" id="gtoc-picker">${gtoc}</div>
</li>
`;
}
function tocPicker(id, content) {
if (id === 'index') {
return '';
}
return `
<li class="picker-header">
<a href="#toc-picker" aria-controls="toc-picker">
<span class="picker-arrow"></span>
Table of contents
</a>
<div class="picker" tabindex="-1">${content.tocPicker.replace('<ul', '<ul id="toc-picker"')}</div>
</li>
`;
}

507
tools/doc/json.mjs Normal file
View File

@ -0,0 +1,507 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
import html from 'remark-html';
import { unified } from 'unified';
import { selectAll } from 'unist-util-select';
import * as common from './common.mjs';
// Unified processor: input is https://github.com/syntax-tree/mdast,
// output is: https://gist.github.com/1777387.
export function jsonAPI({ filename }) {
return (tree, file) => {
const exampleHeading = /^example/i;
const metaExpr = /<!--([^=]+)=([^-]+)-->\n*/g;
const stabilityExpr = /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/s;
// Extract definitions.
const definitions = selectAll('definition', tree);
// Determine the start, stop, and depth of each section.
const sections = [];
let section = null;
tree.children.forEach((node, i) => {
if (node.type === 'heading' &&
!exampleHeading.test(textJoin(node.children, file))) {
if (section) section.stop = i - 1;
section = { start: i, stop: tree.children.length, depth: node.depth };
sections.push(section);
}
});
// Collect and capture results.
const result = { type: 'module', source: filename };
while (sections.length > 0) {
doSection(sections.shift(), result);
}
file.json = result;
// Process a single section (recursively, including subsections).
function doSection(section, parent) {
if (section.depth - parent.depth > 1) {
throw new Error('Inappropriate heading level\n' +
JSON.stringify(section));
}
const current = newSection(tree.children[section.start], file);
let nodes = tree.children.slice(section.start + 1, section.stop + 1);
// Sometimes we have two headings with a single blob of description.
// Treat as a clone.
if (
nodes.length === 0 && sections.length > 0 &&
section.depth === sections[0].depth
) {
nodes = tree.children.slice(sections[0].start + 1,
sections[0].stop + 1);
}
// Extract (and remove) metadata that is not directly inferable
// from the markdown itself.
nodes.forEach((node, i) => {
// Input: <!-- name=module -->; output: {name: module}.
if (node.type === 'html') {
node.value = node.value.replace(metaExpr, (_0, key, value) => {
current[key.trim()] = value.trim();
return '';
});
if (!node.value.trim()) delete nodes[i];
}
// Process metadata:
// <!-- YAML
// added: v1.0.0
// -->
if (node.type === 'html' && common.isYAMLBlock(node.value)) {
current.meta = common.extractAndParseYAML(node.value);
delete nodes[i];
}
// Stability marker: > Stability: ...
if (
node.type === 'blockquote' && node.children.length === 1 &&
node.children[0].type === 'paragraph' &&
nodes.slice(0, i).every((node) => node.type === 'list')
) {
const text = textJoin(node.children[0].children, file);
const stability = stabilityExpr.exec(text);
if (stability) {
current.stability = parseInt(stability[1], 10);
current.stabilityText = stability[2].replaceAll('\n', ' ').trim();
delete nodes[i];
}
}
});
// Compress the node array.
nodes = nodes.filter(() => true);
// If the first node is a list, extract it.
const list = nodes[0] && nodes[0].type === 'list' ?
nodes.shift() : null;
// Now figure out what this list actually means.
// Depending on the section type, the list could be different things.
const values = list ?
list.children.map((child) => parseListItem(child, file)) : [];
switch (current.type) {
case 'ctor':
case 'classMethod':
case 'method': {
// Each item is an argument, unless the name is 'return',
// in which case it's the return value.
const sig = {};
sig.params = values.filter((value) => {
if (value.name === 'return') {
sig.return = value;
return false;
}
return true;
});
parseSignature(current.textRaw, sig);
current.signatures = [sig];
break;
}
case 'property':
// There should be only one item, which is the value.
// Copy the data up to the section.
if (values.length) {
const signature = values[0];
// Shove the name in there for properties,
// since they are always just going to be the value etc.
signature.textRaw = `\`${current.name}\` ${signature.textRaw}`;
for (const key in signature) {
if (signature[key]) {
if (key === 'type') {
current.typeof = signature.type;
} else {
current[key] = signature[key];
}
}
}
}
break;
case 'event':
// Event: each item is an argument.
current.params = values;
break;
default:
// If list wasn't consumed, put it back in the nodes list.
if (list) nodes.unshift(list);
}
// Convert remaining nodes to a 'desc'.
// Unified expects to process a string; but we ignore that as we
// already have pre-parsed input that we can inject.
if (nodes.length) {
if (current.desc) current.shortDesc = current.desc;
current.desc = unified()
.use(function() {
this.Parser = () => (
{ type: 'root', children: nodes.concat(definitions) }
);
})
.use(html, { sanitize: false })
.processSync('').toString().trim();
if (!current.desc) delete current.desc;
}
// Process subsections.
while (sections.length > 0 && sections[0].depth > section.depth) {
doSection(sections.shift(), current);
}
// If type is not set, default type based on parent type, and
// set displayName and name properties.
if (!current.type) {
current.type = (parent.type === 'misc' ? 'misc' : 'module');
current.displayName = current.name;
current.name = current.name.toLowerCase()
.trim().replace(/\s+/g, '_');
}
// Pluralize type to determine which 'bucket' to put this section in.
let plur;
if (current.type.slice(-1) === 's') {
plur = `${current.type}es`;
} else if (current.type.slice(-1) === 'y') {
plur = current.type.replace(/y$/, 'ies');
} else {
plur = `${current.type}s`;
}
// Classes sometimes have various 'ctor' children
// which are actually just descriptions of a constructor class signature.
// Merge them into the parent.
if (current.type === 'class' && current.ctors) {
current.signatures ||= [];
const sigs = current.signatures;
current.ctors.forEach((ctor) => {
ctor.signatures ||= [{}];
ctor.signatures.forEach((sig) => {
sig.desc = ctor.desc;
});
sigs.push(...ctor.signatures);
});
delete current.ctors;
}
// Properties are a bit special.
// Their "type" is the type of object, not "property".
if (current.type === 'property') {
if (current.typeof) {
current.type = current.typeof;
delete current.typeof;
} else {
delete current.type;
}
}
// If the parent's type is 'misc', then it's just a random
// collection of stuff, like the "globals" section.
// Make the children top-level items.
if (current.type === 'misc') {
Object.keys(current).forEach((key) => {
switch (key) {
case 'textRaw':
case 'name':
case 'type':
case 'desc':
case 'miscs':
return;
default:
if (parent.type === 'misc') {
return;
}
if (parent[key] && Array.isArray(parent[key])) {
parent[key] = parent[key].concat(current[key]);
} else {
parent[key] ||= current[key];
}
}
});
}
// Add this section to the parent. Sometimes we have two headings with a
// single blob of description. If the preceding entry at this level
// shares a name and is lacking a description, copy it backwards.
parent[plur] ||= [];
const prev = parent[plur].slice(-1)[0];
if (prev && prev.name === current.name && !prev.desc) {
prev.desc = current.desc;
}
parent[plur].push(current);
}
};
}
const paramExpr = /\((.+)\);?$/;
// text: "someobject.someMethod(a[, b=100][, c])"
function parseSignature(text, sig) {
const list = [];
let [, sigParams] = text.match(paramExpr) || [];
if (!sigParams) return;
sigParams = sigParams.split(',');
let optionalLevel = 0;
const optionalCharDict = { '[': 1, ' ': 0, ']': -1 };
sigParams.forEach((sigParam, i) => {
sigParam = sigParam.trim();
if (!sigParam) {
throw new Error(`Empty parameter slot: ${text}`);
}
let listParam = sig.params[i];
let optional = false;
let defaultValue;
// For grouped optional params such as someMethod(a[, b[, c]]).
let pos;
for (pos = 0; pos < sigParam.length; pos++) {
const levelChange = optionalCharDict[sigParam[pos]];
if (levelChange === undefined) break;
optionalLevel += levelChange;
}
sigParam = sigParam.substring(pos);
optional = (optionalLevel > 0);
for (pos = sigParam.length - 1; pos >= 0; pos--) {
const levelChange = optionalCharDict[sigParam[pos]];
if (levelChange === undefined) break;
optionalLevel += levelChange;
}
sigParam = sigParam.substring(0, pos + 1);
const eq = sigParam.indexOf('=');
if (eq !== -1) {
defaultValue = sigParam.substring(eq + 1);
sigParam = sigParam.substring(0, eq);
}
// At this point, the name should match. If it doesn't find one that does.
// Example: shared signatures for:
// ### new Console(stdout[, stderr][, ignoreErrors])
// ### new Console(options)
if (!listParam || sigParam !== listParam.name) {
listParam = null;
for (const param of sig.params) {
if (param.name === sigParam) {
listParam = param;
} else if (param.options) {
for (const option of param.options) {
if (option.name === sigParam) {
listParam = Object.assign({}, option);
}
}
}
}
if (!listParam) {
if (sigParam.startsWith('...')) {
listParam = { name: sigParam };
} else {
throw new Error(
`Invalid param "${sigParam}"\n` +
` > ${JSON.stringify(listParam)}\n` +
` > ${text}`,
);
}
}
}
if (optional) listParam.optional = true;
if (defaultValue !== undefined) listParam.default = defaultValue.trim();
list.push(listParam);
});
sig.params = list;
}
const returnExpr = /^returns?\s*:?\s*/i;
const nameExpr = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/;
const typeExpr = /^\{([^}]+)\}\s*/;
const leadingHyphen = /^-\s*/;
const defaultExpr = /\s*\*\*Default:\*\*\s*([^]+)$/i;
function parseListItem(item, file) {
const current = {};
current.textRaw = item.children.filter((node) => node.type !== 'list')
.map((node) => (
file.value.slice(node.position.start.offset, node.position.end.offset)),
)
.join('').replace(/\s+/g, ' ').replace(/<!--.*?-->/sg, '');
let text = current.textRaw;
if (!text) {
throw new Error(`Empty list item: ${JSON.stringify(item)}`);
}
// The goal here is to find the name, type, default.
// Anything left over is 'desc'.
if (returnExpr.test(text)) {
current.name = 'return';
text = text.replace(returnExpr, '');
} else {
const [, name] = text.match(nameExpr) || [];
if (name) {
current.name = name;
text = text.replace(nameExpr, '');
}
}
const [, type] = text.match(typeExpr) || [];
if (type) {
current.type = type;
text = text.replace(typeExpr, '');
}
text = text.replace(leadingHyphen, '');
const [, defaultValue] = text.match(defaultExpr) || [];
if (defaultValue) {
current.default = defaultValue.replace(/\.$/, '');
text = text.replace(defaultExpr, '');
}
if (text) current.desc = text;
const options = item.children.find((child) => child.type === 'list');
if (options) {
current.options = options.children.map((child) => (
parseListItem(child, file)
));
}
return current;
}
// This section parses out the contents of an H# tag.
// To reduce escape slashes in RegExp string components.
const r = String.raw;
const eventPrefix = '^Event: +';
const classPrefix = '^[Cc]lass: +';
const ctorPrefix = '^(?:[Cc]onstructor: +)?`?new +';
const classMethodPrefix = '^Static method: +';
const maybeClassPropertyPrefix = '(?:Class property: +)?';
const maybeQuote = '[\'"]?';
const notQuotes = '[^\'"]+';
const maybeBacktick = '`?';
// To include constructs like `readable\[Symbol.asyncIterator\]()`
// or `readable.\_read(size)` (with Markdown escapes).
const simpleId = r`(?:(?:\\?_)+|\b)\w+\b`;
const computedId = r`\\?\[[\w\.]+\\?\]`;
const id = `(?:${simpleId}|${computedId})`;
const classId = r`[A-Z]\w+`;
const ancestors = r`(?:${id}\.?)+`;
const maybeAncestors = r`(?:${id}\.?)*`;
const callWithParams = r`\([^)]*\)`;
const maybeExtends = `(?: +extends +${maybeAncestors}${classId})?`;
const headingExpressions = [
{ type: 'event', re: RegExp(
`${eventPrefix}${maybeBacktick}${maybeQuote}(${notQuotes})${maybeQuote}${maybeBacktick}$`, 'i') },
{ type: 'class', re: RegExp(
`${classPrefix}${maybeBacktick}(${maybeAncestors}${classId})${maybeExtends}${maybeBacktick}$`, '') },
{ type: 'ctor', re: RegExp(
`${ctorPrefix}(${maybeAncestors}${classId})${callWithParams}${maybeBacktick}$`, '') },
{ type: 'classMethod', re: RegExp(
`${classMethodPrefix}${maybeBacktick}${maybeAncestors}(${id})${callWithParams}${maybeBacktick}$`, 'i') },
{ type: 'method', re: RegExp(
`^${maybeBacktick}${maybeAncestors}(${id})${callWithParams}${maybeBacktick}$`, 'i') },
{ type: 'property', re: RegExp(
`^${maybeClassPropertyPrefix}${maybeBacktick}${ancestors}(${id})${maybeBacktick}$`, 'i') },
];
function newSection(header, file) {
const text = textJoin(header.children, file);
// Infer the type from the text.
for (const { type, re } of headingExpressions) {
const [, name] = text.match(re) || [];
if (name) {
return { textRaw: text, type, name };
}
}
return { textRaw: text, name: text };
}
function textJoin(nodes, file) {
return nodes.map((node) => {
if (node.type === 'linkReference') {
return file.value.slice(node.position.start.offset,
node.position.end.offset);
} else if (node.type === 'inlineCode') {
return `\`${node.value}\``;
} else if (node.type === 'strong') {
return `**${textJoin(node.children, file)}**`;
} else if (node.type === 'emphasis') {
return `_${textJoin(node.children, file)}_`;
} else if (node.children) {
return textJoin(node.children, file);
}
return node.value;
}).join('');
}

View File

@ -0,0 +1,6 @@
{
"doc/api/synopsis.md": {
"command line options": "cli.html#options",
"web server": "http.html"
}
}

23
tools/doc/markdown.mjs Normal file
View File

@ -0,0 +1,23 @@
import { visit } from 'unist-util-visit';
export const referenceToLocalMdFile = /^(?![+a-z]+:)([^#?]+)\.md(#.+)?$/i;
export function replaceLinks({ filename, linksMapper }) {
return (tree) => {
const fileHtmlUrls = linksMapper[filename];
visit(tree, (node) => {
node.url &&= node.url.replace(
referenceToLocalMdFile,
(_, filename, hash) => `${filename}.html${hash || ''}`,
);
});
visit(tree, 'definition', (node) => {
const htmlUrl = fileHtmlUrls?.[node.identifier];
if (htmlUrl && typeof htmlUrl === 'string') {
node.url = htmlUrl;
}
});
};
}

1796
tools/doc/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
tools/doc/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
"name": "node-doc-generator",
"description": "Internal tool for generating Node.js API docs",
"version": "0.0.0",
"engines": {
"node": ">=14.8.0"
},
"devDependencies": {
"highlight.js": "^11.11.1",
"js-yaml": "^4.1.0",
"rehype-raw": "^7.0.0",
"rehype-stringify": "^10.0.1",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-html": "^16.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"to-vfile": "^8.0.0",
"unified": "^11.0.5",
"unist-util-select": "^5.1.0",
"unist-util-visit": "^5.0.0"
},
"bin": {
"node-doc-generator": "generate.js"
}
}

118
tools/doc/stability.mjs Normal file
View File

@ -0,0 +1,118 @@
// Build stability table to documentation.html/json/md by generated all.json
import fs from 'fs';
import raw from 'rehype-raw';
import htmlStringify from 'rehype-stringify';
import gfm from 'remark-gfm';
import markdown from 'remark-parse';
import remark2rehype from 'remark-rehype';
import { unified } from 'unified';
import { visit } from 'unist-util-visit';
const source = new URL('../../out/doc/api/', import.meta.url);
const data = JSON.parse(fs.readFileSync(new URL('./all.json', source), 'utf8'));
const markBegin = '<!-- STABILITY_OVERVIEW_SLOT_BEGIN -->';
const markEnd = '<!-- STABILITY_OVERVIEW_SLOT_END -->';
const mark = `${markBegin}(.*)${markEnd}`;
const output = {
json: new URL('./stability.json', source),
docHTML: new URL('./documentation.html', source),
docJSON: new URL('./documentation.json', source),
docMarkdown: new URL('./documentation.md', source),
};
function collectStability(data) {
const stability = [];
for (const mod of data.modules) {
if (mod.displayName && mod.stability >= 0) {
const link = mod.source.replace('doc/api/', '').replace('.md', '.html');
let { stabilityText } = mod;
if (stabilityText.includes('. ')) {
stabilityText = stabilityText.slice(0, stabilityText.indexOf('.'));
}
stability.push({
api: mod.name,
displayName: mod.textRaw,
link: link,
stability: mod.stability,
stabilityText: `(${mod.stability}) ${stabilityText}`,
});
}
}
stability.sort((a, b) => a.displayName.localeCompare(b.displayName));
return stability;
}
function createMarkdownTable(data) {
const md = ['| API | Stability |', '| --- | --------- |'];
for (const mod of data) {
md.push(`| [${mod.displayName}](${mod.link}) | ${mod.stabilityText} |`);
}
return md.join('\n');
}
function createHTML(md) {
const file = unified()
.use(markdown)
.use(gfm)
.use(remark2rehype, { allowDangerousHtml: true })
.use(raw)
.use(htmlStringify)
.use(processStability)
.processSync(md);
return file.value.trim();
}
function processStability() {
return (tree) => {
visit(tree, { type: 'element', tagName: 'tr' }, (node) => {
const [api, stability] = node.children;
if (api.tagName !== 'td') {
return;
}
api.properties.class = 'module_stability';
const level = stability.children[0].value[1];
stability.properties.class = `api_stability api_stability_${level}`;
});
};
}
function updateStabilityMark(file, value, mark) {
const fd = fs.openSync(file, 'r+');
const content = fs.readFileSync(fd, { encoding: 'utf8' });
const replaced = content.replace(mark, value);
if (replaced !== content) {
fs.writeSync(fd, replaced, 0, 'utf8');
}
fs.closeSync(fd);
}
const stability = collectStability(data);
// add markdown
const markdownTable = createMarkdownTable(stability);
updateStabilityMark(output.docMarkdown,
`${markBegin}\n${markdownTable}\n${markEnd}`,
new RegExp(mark, 's'));
// add html table
const html = createHTML(markdownTable);
updateStabilityMark(output.docHTML, `${markBegin}${html}${markEnd}`,
new RegExp(mark, 's'));
// add json output
updateStabilityMark(output.docJSON,
JSON.stringify(`${markBegin}${html}${markEnd}`),
new RegExp(JSON.stringify(mark), 's'));

369
tools/doc/type-parser.mjs Normal file
View File

@ -0,0 +1,369 @@
const jsDocPrefix = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/';
const jsDataStructuresUrl = `${jsDocPrefix}Data_structures`;
const jsPrimitives = {
boolean: 'Boolean',
integer: 'Number', // Not a primitive, used for clarification.
null: 'Null',
number: 'Number',
string: 'String',
symbol: 'Symbol',
undefined: 'Undefined',
};
const jsGlobalObjectsUrl = `${jsDocPrefix}Reference/Global_Objects/`;
const jsGlobalTypes = [
'AggregateError', 'Array', 'ArrayBuffer', 'DataView', 'Date', 'Error',
'EvalError', 'Function', 'Map', 'NaN', 'Object', 'Promise', 'Proxy', 'RangeError',
'ReferenceError', 'RegExp', 'Set', 'SharedArrayBuffer', 'SyntaxError', 'Symbol',
'TypeError', 'URIError', 'WeakMap', 'WeakSet',
'TypedArray',
'Float16Array', 'Float32Array', 'Float64Array',
'Int8Array', 'Int16Array', 'Int32Array',
'Uint8Array', 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array',
];
const customTypesMap = {
'any': `${jsDataStructuresUrl}#Data_types`,
'this': `${jsDocPrefix}Reference/Operators/this`,
'AbortController': 'globals.html#class-abortcontroller',
'AbortSignal': 'globals.html#class-abortsignal',
'ArrayBufferView':
'https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView',
'AsyncIterator': 'https://tc39.github.io/ecma262/#sec-asynciterator-interface',
'AsyncIterable': 'https://tc39.github.io/ecma262/#sec-asynciterable-interface',
'AsyncFunction': 'https://tc39.es/ecma262/#sec-async-function-constructor',
'AsyncGeneratorFunction': 'https://tc39.es/proposal-async-iteration/#sec-asyncgeneratorfunction-constructor',
'bigint': `${jsDocPrefix}Reference/Global_Objects/BigInt`,
'WebAssembly.Instance':
`${jsDocPrefix}Reference/Global_Objects/WebAssembly/Instance`,
'Blob': 'buffer.html#class-blob',
'File': 'buffer.html#class-file',
'BroadcastChannel':
'worker_threads.html#class-broadcastchannel-' +
'extends-eventtarget',
'Iterable':
`${jsDocPrefix}Reference/Iteration_protocols#The_iterable_protocol`,
'Iterator':
`${jsDocPrefix}Reference/Iteration_protocols#The_iterator_protocol`,
'Module Namespace Object':
'https://tc39.github.io/ecma262/#sec-module-namespace-exotic-objects',
'AsyncLocalStorage': 'async_context.html#class-asynclocalstorage',
'AsyncHook': 'async_hooks.html#async_hookscreatehookcallbacks',
'AsyncResource': 'async_hooks.html#class-asyncresource',
'brotli options': 'zlib.html#class-brotlioptions',
'Buffer': 'buffer.html#class-buffer',
'ChildProcess': 'child_process.html#class-childprocess',
'cluster.Worker': 'cluster.html#class-worker',
'Cipheriv': 'crypto.html#class-cipheriv',
'Decipheriv': 'crypto.html#class-decipheriv',
'DiffieHellman': 'crypto.html#class-diffiehellman',
'DiffieHellmanGroup': 'crypto.html#class-diffiehellmangroup',
'ECDH': 'crypto.html#class-ecdh',
'Hash': 'crypto.html#class-hash',
'Hmac': 'crypto.html#class-hmac',
'KeyObject': 'crypto.html#class-keyobject',
'Sign': 'crypto.html#class-sign',
'Verify': 'crypto.html#class-verify',
'crypto.constants': 'crypto.html#cryptoconstants',
'Algorithm': 'webcrypto.html#class-algorithm',
'CryptoKey': 'webcrypto.html#class-cryptokey',
'CryptoKeyPair': 'webcrypto.html#class-cryptokeypair',
'Crypto': 'webcrypto.html#class-crypto',
'SubtleCrypto': 'webcrypto.html#class-subtlecrypto',
'RsaOaepParams': 'webcrypto.html#class-rsaoaepparams',
'AesCtrParams': 'webcrypto.html#class-aesctrparams',
'AesCbcParams': 'webcrypto.html#class-aescbcparams',
'AesDerivedKeyParams': 'webcrypto.html#class-aesderivedkeyparams',
'AesGcmParams': 'webcrypto.html#class-aesgcmparams',
'EcdhKeyDeriveParams': 'webcrypto.html#class-ecdhkeyderiveparams',
'HkdfParams': 'webcrypto.html#class-hkdfparams',
'KeyAlgorithm': 'webcrypto.html#class-keyalgorithm',
'Pbkdf2Params': 'webcrypto.html#class-pbkdf2params',
'HmacKeyAlgorithm': 'webcrypto.html#class-hmackeyalgorithm',
'HmacKeyGenParams': 'webcrypto.html#class-hmackeygenparams',
'AesKeyAlgorithm': 'webcrypto.html#class-aeskeyalgorithm',
'AesKeyGenParams': 'webcrypto.html#class-aeskeygenparams',
'RsaHashedKeyAlgorithm':
'webcrypto.html#class-rsahashedkeyalgorithm',
'RsaHashedKeyGenParams':
'webcrypto.html#class-rsahashedkeygenparams',
'EcKeyAlgorithm': 'webcrypto.html#class-eckeyalgorithm',
'EcKeyGenParams': 'webcrypto.html#class-eckeygenparams',
'RsaHashedImportParams':
'webcrypto.html#class-rsahashedimportparams',
'EcKeyImportParams': 'webcrypto.html#class-eckeyimportparams',
'HmacImportParams': 'webcrypto.html#class-hmacimportparams',
'EcdsaParams': 'webcrypto.html#class-ecdsaparams',
'RsaPssParams': 'webcrypto.html#class-rsapssparams',
'Ed448Params': 'webcrypto.html#class-ed448params',
'dgram.Socket': 'dgram.html#class-dgramsocket',
'Channel': 'diagnostics_channel.html#class-channel',
'TracingChannel': 'diagnostics_channel.html#class-tracingchannel',
'DatabaseSync': 'sqlite.html#class-databasesync',
'Domain': 'domain.html#class-domain',
'errors.Error': 'errors.html#class-error',
'import.meta': 'esm.html#importmeta',
'EventEmitter': 'events.html#class-eventemitter',
'EventTarget': 'events.html#class-eventtarget',
'Event': 'events.html#class-event',
'CustomEvent': 'events.html#class-customevent',
'EventListener': 'events.html#event-listener',
'FileHandle': 'fs.html#class-filehandle',
'fs.Dir': 'fs.html#class-fsdir',
'fs.Dirent': 'fs.html#class-fsdirent',
'fs.FSWatcher': 'fs.html#class-fsfswatcher',
'fs.ReadStream': 'fs.html#class-fsreadstream',
'fs.Stats': 'fs.html#class-fsstats',
'fs.StatFs': 'fs.html#class-fsstatfs',
'fs.StatWatcher': 'fs.html#class-fsstatwatcher',
'fs.WriteStream': 'fs.html#class-fswritestream',
'http.Agent': 'http.html#class-httpagent',
'http.ClientRequest': 'http.html#class-httpclientrequest',
'http.IncomingMessage': 'http.html#class-httpincomingmessage',
'http.OutgoingMessage': 'http.html#class-httpoutgoingmessage',
'http.Server': 'http.html#class-httpserver',
'http.ServerResponse': 'http.html#class-httpserverresponse',
'ClientHttp2Session': 'http2.html#class-clienthttp2session',
'ClientHttp2Stream': 'http2.html#class-clienthttp2stream',
'HTTP/2 Headers Object': 'http2.html#headers-object',
'HTTP/2 Settings Object': 'http2.html#settings-object',
'http2.Http2ServerRequest': 'http2.html#class-http2http2serverrequest',
'http2.Http2ServerResponse':
'http2.html#class-http2http2serverresponse',
'Http2SecureServer': 'http2.html#class-http2secureserver',
'Http2Server': 'http2.html#class-http2server',
'Http2Session': 'http2.html#class-http2session',
'Http2Stream': 'http2.html#class-http2stream',
'ServerHttp2Stream': 'http2.html#class-serverhttp2stream',
'ServerHttp2Session': 'http2.html#class-serverhttp2session',
'https.Server': 'https.html#class-httpsserver',
'module': 'modules.html#the-module-object',
'module.SourceMap':
'module.html#class-modulesourcemap',
'MockModuleContext': 'test.html#class-mockmodulecontext',
'require': 'modules.html#requireid',
'Handle': 'net.html#serverlistenhandle-backlog-callback',
'net.BlockList': 'net.html#class-netblocklist',
'net.Server': 'net.html#class-netserver',
'net.Socket': 'net.html#class-netsocket',
'net.SocketAddress': 'net.html#class-netsocketaddress',
'NodeEventTarget':
'events.html#class-nodeeventtarget',
'os.constants.dlopen': 'os.html#dlopen-constants',
'Histogram': 'perf_hooks.html#class-histogram',
'IntervalHistogram':
'perf_hooks.html#class-intervalhistogram-extends-histogram',
'RecordableHistogram':
'perf_hooks.html#class-recordablehistogram-extends-histogram',
'PerformanceEntry': 'perf_hooks.html#class-performanceentry',
'PerformanceNodeTiming':
'perf_hooks.html#class-performancenodetiming',
'PerformanceObserver':
'perf_hooks.html#class-performanceobserver',
'PerformanceObserverEntryList':
'perf_hooks.html#class-performanceobserverentrylist',
'readline.Interface':
'readline.html#class-readlineinterface',
'readline.InterfaceConstructor':
'readline.html#class-interfaceconstructor',
'readlinePromises.Interface':
'readline.html#class-readlinepromisesinterface',
'repl.REPLServer': 'repl.html#class-replserver',
'Session': 'sqlite.html#class-session',
'StatementSync': 'sqlite.html#class-statementsync',
'Stream': 'stream.html#stream',
'stream.Duplex': 'stream.html#class-streamduplex',
'Duplex': 'stream.html#class-streamduplex',
'stream.Readable': 'stream.html#class-streamreadable',
'Readable': 'stream.html#class-streamreadable',
'stream.Transform': 'stream.html#class-streamtransform',
'Transform': 'stream.html#class-streamtransform',
'stream.Writable': 'stream.html#class-streamwritable',
'Writable': 'stream.html#class-streamwritable',
'Immediate': 'timers.html#class-immediate',
'Timeout': 'timers.html#class-timeout',
'Timer': 'timers.html#timers',
'TestsStream': 'test.html#class-testsstream',
'tls.SecureContext': 'tls.html#tlscreatesecurecontextoptions',
'tls.Server': 'tls.html#class-tlsserver',
'tls.TLSSocket': 'tls.html#class-tlstlssocket',
'Tracing': 'tracing.html#tracing-object',
'tty.ReadStream': 'tty.html#class-ttyreadstream',
'tty.WriteStream': 'tty.html#class-ttywritestream',
'URL': 'url.html#the-whatwg-url-api',
'URLSearchParams': 'url.html#class-urlsearchparams',
'MIMEParams': 'util.html#class-utilmimeparams',
'vm.Module': 'vm.html#class-vmmodule',
'vm.Script': 'vm.html#class-vmscript',
'vm.SourceTextModule': 'vm.html#class-vmsourcetextmodule',
'vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER':
'vm.html#vmconstantsuse_main_context_default_loader',
'vm.constants.DONT_CONTEXTIFY':
'vm.html#vmconstantsdont_contextify',
'MessagePort': 'worker_threads.html#class-messageport',
'Worker': 'worker_threads.html#class-worker',
'X509Certificate': 'crypto.html#class-x509certificate',
'zlib options': 'zlib.html#class-options',
'zstd options': 'zlib.html#class-zstdoptions',
'ReadableStream':
'webstreams.html#class-readablestream',
'ReadableStreamDefaultReader':
'webstreams.html#class-readablestreamdefaultreader',
'ReadableStreamBYOBReader':
'webstreams.html#class-readablestreambyobreader',
'ReadableStreamDefaultController':
'webstreams.html#class-readablestreamdefaultcontroller',
'ReadableByteStreamController':
'webstreams.html#class-readablebytestreamcontroller',
'ReadableStreamBYOBRequest':
'webstreams.html#class-readablestreambyobrequest',
'WritableStream':
'webstreams.html#class-writablestream',
'WritableStreamDefaultWriter':
'webstreams.html#class-writablestreamdefaultwriter',
'WritableStreamDefaultController':
'webstreams.html#class-writablestreamdefaultcontroller',
'TransformStream':
'webstreams.html#class-transformstream',
'TransformStreamDefaultController':
'webstreams.html#class-transformstreamdefaultcontroller',
'ByteLengthQueuingStrategy':
'webstreams.html#class-bytelengthqueuingstrategy',
'CountQueuingStrategy':
'webstreams.html#class-countqueuingstrategy',
'TextEncoderStream':
'webstreams.html#class-textencoderstream',
'TextDecoderStream':
'webstreams.html#class-textdecoderstream',
'FormData': 'https://developer.mozilla.org/en-US/docs/Web/API/FormData',
'Headers': 'https://developer.mozilla.org/en-US/docs/Web/API/Headers',
'Response': 'https://developer.mozilla.org/en-US/docs/Web/API/Response',
'Request': 'https://developer.mozilla.org/en-US/docs/Web/API/Request',
'Disposable': 'https://tc39.es/proposal-explicit-resource-management/#sec-disposable-interface',
'quic.QuicEndpoint': 'quic.html#class-quicendpoint',
'quic.QuicEndpoint.Stats': 'quic.html#class-quicendpointstats',
'quic.QuicSession': 'quic.html#class-quicsession',
'quic.QuicSession.Stats': 'quic.html#class-quicsessionstats',
'quic.QuicStream': 'quic.html#class-quicstream',
'quic.QuicStream.Stats': 'quic.html#class-quicstreamstats',
'quic.EndpointOptions': 'quic.html#type-endpointoptions',
'quic.SessionOptions': 'quic.html#type-sessionoptions',
'quic.ApplicationOptions': 'quic.html#type-applicationoptions',
'quic.TlsOptions': 'quic.html#type-tlsoptions',
'quic.TransportParams': 'quic.html#type-transportparams',
'quic.OnSessionCallback': 'quic.html#callback-onsessioncallback',
'quic.OnStreamCallback': 'quic.html#callback-onstreamcallback',
'quic.OnDatagramCallback': 'quic.html#callback-ondatagramcallback',
'quic.OnDatagramStatusCallback': 'quic.html#callback-ondatagramstatuscallback',
'quic.OnPathValidationCallback': 'quic.html#callback-onpathvalidationcallback',
'quic.OnSessionTicketCallback': 'quic.html#callback-onsessionticketcallback',
'quic.OnVersionNegotiationCallback': 'quic.html#callback-onversionnegotiationcallback',
'quic.OnHandshakeCallback': 'quic.html#callback-onhandshakecallback',
'quic.OnBlockedCallback': 'quic.html#callback-onblockedcallback',
'quic.OnStreamErrorCallback': 'quic.html#callback-onstreamerrorcallback',
'quic.OnHeadersCallback': 'quic.html#callback-onheaderscallback',
'quic.OnTrailersCallback': 'quic.html#callback-ontrailerscallback',
'quic.OnPullCallback': 'quic.html#callback-onpullcallback',
};
const arrayPart = /(?:\[])+$/;
export function toLink(typeInput) {
const typeLinks = [];
typeInput = typeInput.replace('{', '').replace('}', '');
const typeTexts = typeInput.split('|');
typeTexts.forEach((typeText) => {
typeText = typeText.trim();
if (typeText) {
let typeUrl;
// To support type[], type[][] etc., we store the full string
// and use the bracket-less version to lookup the type URL.
const typeTextFull = typeText;
typeText = typeText.replace(arrayPart, '');
const primitive = jsPrimitives[typeText];
if (primitive !== undefined) {
typeUrl = `${jsDataStructuresUrl}#${primitive}_type`;
} else if (jsGlobalTypes.includes(typeText)) {
typeUrl = `${jsGlobalObjectsUrl}${typeText}`;
} else {
typeUrl = customTypesMap[typeText];
}
if (typeUrl) {
typeLinks.push(
`<a href="${typeUrl}" class="type">&lt;${typeTextFull}&gt;</a>`);
} else {
throw new Error(
`Unrecognized type: '${typeTextFull}' in '${import.meta.url}'.\n` +
'Valid types can be found at https://github.com/nodejs/node/blob/HEAD/tools/doc/type-parser.mjs',
);
}
} else {
throw new Error(`Empty type slot: ${typeInput}`);
}
});
return typeLinks.length ? typeLinks.join(' | ') : typeInput;
}

78
tools/doc/versions.mjs Normal file
View File

@ -0,0 +1,78 @@
import { readFileSync, writeFileSync } from 'fs';
import https from 'https';
const srcRoot = new URL('../../', import.meta.url);
const isRelease = () => {
const re = /#define NODE_VERSION_IS_RELEASE 0/;
const file = new URL('./src/node_version.h', srcRoot);
return !re.test(readFileSync(file, { encoding: 'utf8' }));
};
const getUrl = (url) => {
return new Promise((resolve, reject) => {
const request = https.get(url, { timeout: 30000 }, (response) => {
if (response.statusCode !== 200) {
reject(new Error(
`Failed to get ${url}, status code ${response.statusCode}`));
}
response.setEncoding('utf8');
let body = '';
response.on('aborted', () => reject());
response.on('data', (data) => body += data);
response.on('end', () => resolve(body));
});
request.on('error', (err) => reject(err));
request.on('timeout', () => request.abort());
});
};
const kNoInternet = !!process.env.NODE_TEST_NO_INTERNET;
const outFile = (process.argv.length > 2 ? process.argv[2] : undefined);
async function versions() {
// The CHANGELOG.md on release branches may not reference newer semver
// majors of Node.js so fetch and parse the version from the main branch.
const url =
'https://raw.githubusercontent.com/nodejs/node/HEAD/CHANGELOG.md';
let changelog;
const file = new URL('./CHANGELOG.md', srcRoot);
if (kNoInternet) {
changelog = readFileSync(file, { encoding: 'utf8' });
} else {
try {
changelog = await getUrl(url);
} catch (e) {
// Fail if this is a release build, otherwise fallback to local files.
if (isRelease()) {
throw e;
} else {
console.warn(`Unable to retrieve ${url}. Falling back to ${file}.`);
changelog = readFileSync(file, { encoding: 'utf8' });
}
}
}
const ltsRE = /Long Term Support/i;
const versionRE = /\* \[Node\.js ([0-9.]+)\]\S+ (.*)\r?\n/g;
const _versions = [];
let match;
while ((match = versionRE.exec(changelog)) != null) {
const entry = { num: `${match[1]}.x` };
if (ltsRE.test(match[2])) {
entry.lts = true;
}
_versions.push(entry);
}
return _versions;
}
versions().then((v) => {
if (outFile) {
writeFileSync(outFile, JSON.stringify(v));
} else {
console.log(JSON.stringify(v));
}
}).catch((err) => {
console.error(err);
process.exit(1);
});