Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v10.x backport] doc,tools: get altDocs versions from CHANGELOG.md #32642

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -649,15 +649,22 @@ out/doc/api/assets/%: doc/api_assets/% out/doc/api/assets
run-npm-ci = $(PWD)/$(NPM) ci

LINK_DATA = out/doc/apilinks.json
VERSIONS_DATA = out/doc/previous-versions.json
gen-api = tools/doc/generate.js --node-version=$(FULLVERSION) \
--apilinks=$(LINK_DATA) $< --output-directory=out/doc/api
--apilinks=$(LINK_DATA) $< --output-directory=out/doc/api \
--versions-file=$(VERSIONS_DATA)
gen-apilink = tools/doc/apilinks.js $(LINK_DATA) $(wildcard lib/*.js)

$(LINK_DATA): $(wildcard lib/*.js) tools/doc/apilinks.js
$(call available-node, $(gen-apilink))

# Regenerate previous versions data if the current version changes
$(VERSIONS_DATA): CHANGELOG.md src/node_version.h tools/doc/versions.js
$(call available-node, tools/doc/versions.js $@)

out/doc/api/%.json out/doc/api/%.html: doc/api/%.md tools/doc/generate.js \
tools/doc/html.js tools/doc/json.js tools/doc/apilinks.js | $(LINK_DATA)
tools/doc/html.js tools/doc/json.js tools/doc/apilinks.js \
$(VERSIONS_DATA) | $(LINK_DATA)
$(call available-node, $(gen-api))

out/doc/api/all.html: $(apidocs_html) tools/doc/allhtml.js \
Expand Down
43 changes: 22 additions & 21 deletions test/doctool/test-doctool-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const remark2rehype = require('remark-rehype');
const raw = require('rehype-raw');
const htmlStringify = require('rehype-stringify');

function toHTML({ input, filename, nodeVersion }, cb) {
function toHTML({ input, filename, nodeVersion, versions }) {
const content = unified()
.use(markdown)
.use(html.firstHeader)
Expand All @@ -34,10 +34,7 @@ function toHTML({ input, filename, nodeVersion }, cb) {
.use(htmlStringify)
.processSync(input);

html.toHTML(
{ input, content, filename, nodeVersion },
cb
);
return html.toHTML({ input, content, filename, nodeVersion, versions });
}

// Test data is a list of objects with two properties.
Expand Down Expand Up @@ -102,28 +99,32 @@ const testData = [
];

const spaces = /\s/g;
const versions = [
{ num: '10.x', lts: true },
{ num: '9.x' },
{ num: '8.x' },
{ num: '7.x' },
{ num: '6.x' },
{ num: '5.x' },
{ num: '4.x' },
{ num: '0.12.x' },
{ num: '0.10.x' }];

testData.forEach(({ file, html }) => {
// Normalize expected data by stripping whitespace.
const expected = html.replace(spaces, '');

readFile(file, 'utf8', common.mustCall((err, input) => {
readFile(file, 'utf8', common.mustCall(async (err, input) => {
assert.ifError(err);
toHTML(
{
input: input,
filename: 'foo',
nodeVersion: process.version,
},
common.mustCall((err, output) => {
assert.ifError(err);
const output = toHTML({ input: input,
filename: 'foo',
nodeVersion: process.version,
versions: versions });

const actual = output.replace(spaces, '');
// Assert that the input stripped of all whitespace contains the
// expected markup.
assert(actual.includes(expected),
`ACTUAL: ${actual}\nEXPECTED: ${expected}`);
})
);
const actual = output.replace(spaces, '');
// Assert that the input stripped of all whitespace contains the
// expected markup.
assert(actual.includes(expected),
`ACTUAL: ${actual}\nEXPECTED: ${expected}`);
}));
});
74 changes: 74 additions & 0 deletions test/doctool/test-doctool-versions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use strict';

require('../common');
const assert = require('assert');
const { spawnSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');
const util = require('util');

const debuglog = util.debuglog('test');
const versionsTool = path.resolve(__dirname, '../../tools/doc/versions.js');

// At the time of writing these are the minimum expected versions.
// New versions of Node.js do not have to be explicitly added here.
const expected = [
'12.x',
'11.x',
'10.x',
'9.x',
'8.x',
'7.x',
'6.x',
'5.x',
'4.x',
'0.12.x',
'0.10.x',
];

tmpdir.refresh();
const versionsFile = path.join(tmpdir.path, 'versions.json');
debuglog(`${process.execPath} ${versionsTool} ${versionsFile}`);
const opts = { cwd: tmpdir.path, encoding: 'utf8' };
const cp = spawnSync(process.execPath, [ versionsTool, versionsFile ], opts);
debuglog(cp.stderr);
debuglog(cp.stdout);
assert.strictEqual(cp.stdout, '');
assert.strictEqual(cp.signal, null);
assert.strictEqual(cp.status, 0);
const versions = JSON.parse(fs.readFileSync(versionsFile));
debuglog(versions);

// Coherence checks for each returned version.
for (const version of versions) {
const tested = util.inspect(version);
const parts = version.num.split('.');
const expectedLength = parts[0] === '0' ? 3 : 2;
assert.strictEqual(parts.length, expectedLength,
`'num' from ${tested} should be '<major>.x'.`);
assert.strictEqual(parts[parts.length - 1], 'x',
`'num' from ${tested} doesn't end in '.x'.`);
const isEvenRelease = Number.parseInt(parts[expectedLength - 2]) % 2 === 0;
const hasLtsProperty = version.hasOwnProperty('lts');
if (hasLtsProperty) {
// Odd-numbered versions of Node.js are never LTS.
assert.ok(isEvenRelease, `${tested} should not be an 'lts' release.`);
assert.ok(version.lts, `'lts' from ${tested} should 'true'.`);
}
}

// Check that the minimum number of versions were returned.
// Later versions are allowed, but not checked for here (they were checked
// above).
// Also check for the previous semver major -- From master this will be the
// most recent major release.
const thisMajor = Number.parseInt(process.versions.node.split('.')[0]);
const prevMajorString = `${thisMajor - 1}.x`;
if (!expected.includes(prevMajorString)) {
expected.unshift(prevMajorString);
}
for (const version of expected) {
assert.ok(versions.find((x) => x.num === version),
`Did not find entry for '${version}' in ${util.inspect(versions)}`);
}
26 changes: 15 additions & 11 deletions tools/doc/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ let filename = null;
let nodeVersion = null;
let outputDir = null;
let apilinks = {};
let versions = {};

args.forEach(function(arg) {
if (!arg.startsWith('--')) {
Expand All @@ -55,6 +56,13 @@ args.forEach(function(arg) {
throw new Error(`${linkFile} is empty`);
}
apilinks = JSON.parse(data);
} else if (arg.startsWith('--versions-file=')) {
const versionsFile = arg.replace(/^--versions-file=/, '');
const data = fs.readFileSync(versionsFile, 'utf8');
if (!data.trim()) {
throw new Error(`${versionsFile} is empty`);
}
versions = JSON.parse(data);
}
});

Expand All @@ -67,7 +75,7 @@ if (!filename) {
}


fs.readFile(filename, 'utf8', (er, input) => {
fs.readFile(filename, 'utf8', async (er, input) => {
if (er) throw er;

const content = unified()
Expand All @@ -84,15 +92,11 @@ fs.readFile(filename, 'utf8', (er, input) => {

const basename = path.basename(filename, '.md');

html.toHTML(
{ input, content, filename, nodeVersion },
(err, html) => {
const target = path.join(outputDir, `${basename}.html`);
if (err) throw err;
fs.writeFileSync(target, html);
}
);
const myHtml = html.toHTML({ input, content, filename, nodeVersion,
versions });
const htmlTarget = path.join(outputDir, `${basename}.html`);
fs.writeFileSync(htmlTarget, myHtml);

const target = path.join(outputDir, `${basename}.json`);
fs.writeFileSync(target, JSON.stringify(content.json, null, 2));
const jsonTarget = path.join(outputDir, `${basename}.json`);
fs.writeFileSync(jsonTarget, JSON.stringify(content.json, null, 2));
});
21 changes: 4 additions & 17 deletions tools/doc/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const gtocHTML = unified()
const templatePath = path.join(docPath, 'template.html');
const template = fs.readFileSync(templatePath, 'utf8');

function toHTML({ input, content, filename, nodeVersion }, cb) {
function toHTML({ input, content, filename, nodeVersion, versions }) {
filename = path.basename(filename, '.md');

const id = filename.replace(/\W+/g, '-');
Expand All @@ -80,13 +80,13 @@ function toHTML({ input, content, filename, nodeVersion }, cb) {
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));
HTML = HTML.replace('__ALTDOCS__', altDocs(filename, docCreated, versions));
} else {
console.error(`Failed to add alternative version links to ${filename}`);
HTML = HTML.replace('__ALTDOCS__', '');
}

cb(null, HTML);
return HTML;
}

// Set the section name based on the first header. Default to 'Index'.
Expand Down Expand Up @@ -380,22 +380,9 @@ function getId(text, idCounters) {
return text;
}

function altDocs(filename, docCreated) {
function altDocs(filename, docCreated, versions) {
const [, docCreatedMajor, docCreatedMinor] = docCreated.map(Number);
const host = 'https://nodejs.org';
const versions = [
{ num: '12.x' },
{ num: '11.x' },
{ num: '10.x', lts: true },
{ num: '9.x' },
{ num: '8.x', lts: true },
{ num: '7.x' },
{ num: '6.x' },
{ num: '5.x' },
{ num: '4.x' },
{ num: '0.12.x' },
{ num: '0.10.x' }
];

const getHref = (versionNum) =>
`${host}/docs/latest-v${versionNum}/api/${filename}.html`;
Expand Down
80 changes: 80 additions & 0 deletions tools/doc/versions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use strict';

const { readFileSync, writeFileSync } = require('fs');
const path = require('path');
const srcRoot = path.join(__dirname, '..', '..');

const isRelease = () => {
const re = /#define NODE_VERSION_IS_RELEASE 0/;
const file = path.join(srcRoot, 'src', 'node_version.h');
return !re.test(readFileSync(file, { encoding: 'utf8' }));
};

const getUrl = (url) => {
return new Promise((resolve, reject) => {
const https = require('https');
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 master branch.
const url =
'https://github.com/nodejs/node/master/CHANGELOG.md';
let changelog;
const file = path.join(srcRoot, 'CHANGELOG.md');
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);
});