Skip to content

Commit 4eeea5b

Browse files
committed
feat(index): support passing an index file
1 parent 492c2da commit 4eeea5b

File tree

9 files changed

+174
-75
lines changed

9 files changed

+174
-75
lines changed

bin/commands/generate.mjs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { publicGenerators } from '../../src/generators/index.mjs';
1111
import createGenerator from '../../src/generators.mjs';
1212
import createLinter from '../../src/linter/index.mjs';
1313
import { getEnabledRules } from '../../src/linter/utils/rules.mjs';
14-
import createNodeReleases from '../../src/releases.mjs';
14+
import { parseChangelog, parseIndex } from '../../src/parsers/markdown.mjs';
1515
import { loadAndParse } from '../utils.mjs';
1616

1717
const availableGenerators = Object.keys(publicGenerators);
@@ -107,6 +107,14 @@ export default {
107107
})),
108108
},
109109
},
110+
index: {
111+
flags: ['--index <path>'],
112+
desc: 'The index document, for getting the titles of various API docs',
113+
prompt: {
114+
message: 'Path to doc/api/index.md',
115+
type: 'text',
116+
},
117+
},
110118
skipLint: {
111119
flags: ['--skip-lint'],
112120
desc: 'Skip lint before generate',
@@ -135,9 +143,8 @@ export default {
135143
process.exit(1);
136144
}
137145

138-
const { getAllMajors } = createNodeReleases(opts.changelog);
139-
140-
const releases = await getAllMajors();
146+
const releases = await parseChangelog(opts.changelog);
147+
const index = opts.index && (await parseIndex(opts.index));
141148

142149
const { runGenerators } = createGenerator(docs);
143150

@@ -149,6 +156,7 @@ export default {
149156
releases,
150157
gitRef: opts.gitRef,
151158
threads: parseInt(opts.threads, 10),
159+
index,
152160
});
153161
},
154162
};

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
"format": "prettier .",
1111
"format:write": "prettier --write .",
1212
"format:check": "prettier --check .",
13-
"test": "node --test",
13+
"test": "node --test --experimental-test-module-mocks",
1414
"test:coverage": "c8 npm test",
15-
"test:ci": "c8 --reporter=lcov node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit.xml --test-reporter=spec --test-reporter-destination=stdout",
16-
"test:update-snapshots": "node --test --test-update-snapshots",
17-
"test:watch": "node --test --watch",
15+
"test:ci": "c8 --reporter=lcov node --test --experimental-test-module-mocks --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit.xml --test-reporter=spec --test-reporter-destination=stdout",
16+
"test:update-snapshots": "node --test --experimental-test-module-mocks --test-update-snapshots",
17+
"test:watch": "node --test --experimental-test-module-mocks --watch",
1818
"prepare": "husky",
1919
"run": "node bin/cli.mjs",
2020
"watch": "node --watch bin/cli.mjs"

src/generators/legacy-html/index.mjs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export default {
4747
* @param {Input} input
4848
* @param {Partial<GeneratorOptions>} options
4949
*/
50-
async generate(input, { releases, version, output }) {
50+
async generate(input, { index, releases, version, output }) {
5151
// This array holds all the generated values for each module
5252
const generatedValues = [];
5353

@@ -68,9 +68,16 @@ export default {
6868
.filter(node => node.heading.depth === 1)
6969
.sort((a, b) => a.heading.data.name.localeCompare(b.heading.data.name));
7070

71+
const indexOfFiles = index
72+
? index.map(entry => ({
73+
api: entry.api,
74+
heading: { data: { depth: 1, name: entry.section } },
75+
}))
76+
: headNodes;
77+
7178
// Generates the global Table of Contents (Sidebar Navigation)
7279
const parsedSideNav = remarkRehypeProcessor.processSync(
73-
tableOfContents(headNodes, {
80+
tableOfContents(indexOfFiles, {
7481
maxDepth: 1,
7582
parser: tableOfContents.parseNavigationNode,
7683
})

src/generators/types.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ declare global {
3131
// A list of all Node.js major versions and their respective release information
3232
releases: Array<ApiDocReleaseEntry>;
3333

34+
// A list of all the titles of all the documentation files
35+
index: Array<{ section: string; api: string }>;
36+
3437
// An URL containing a git ref URL pointing to the commit or ref that was used
3538
// to generate the API docs. This is used to link to the source code of the
3639
// i.e. https://github.com/nodejs/node/tree/2cb1d07e0f6d9456438016bab7db4688ab354fd2
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
import assert from 'node:assert/strict';
4+
import { describe, it, mock } from 'node:test';
5+
6+
import dedent from 'dedent';
7+
8+
let content;
9+
mock.module('../../utils/parser.mjs', {
10+
namedExports: {
11+
loadFromURL: async () => content,
12+
},
13+
});
14+
15+
const { parseChangelog, parseIndex } = await import('../markdown.mjs');
16+
17+
describe('parseChangelog', () => {
18+
it('should parse Node.js versions and their LTS status', async () => {
19+
content = dedent`
20+
* [Node.js 24](doc/changelogs/CHANGELOG_V24.md) **Current**
21+
* [Node.js 22](doc/changelogs/CHANGELOG_V22.md) **Long Term Support**\n
22+
`;
23+
24+
const results = await parseChangelog('...');
25+
26+
assert.partialDeepStrictEqual(results, [
27+
{ version: { raw: '24.0.0' }, isLts: false },
28+
{ version: { raw: '22.0.0' }, isLts: true },
29+
]);
30+
});
31+
});
32+
33+
describe('parseIndex', () => {
34+
it('should retrieve document titles for sidebar generation', async () => {
35+
content = dedent`
36+
# API Documentation
37+
38+
* [Assert](assert.md)
39+
* [Buffer](buffer.md)
40+
* [Child Process](child_process.md)
41+
* [Something](not-a-markdown-file)
42+
`;
43+
44+
const results = await parseIndex('...');
45+
46+
assert.deepStrictEqual(results, [
47+
{ section: 'Assert', api: 'assert' },
48+
{ section: 'Buffer', api: 'buffer' },
49+
{ section: 'Child Process', api: 'child_process' },
50+
]);
51+
});
52+
});

src/parsers/markdown.mjs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
'use strict';
22

3+
import { coerce } from 'semver';
4+
5+
import { loadFromURL } from '../utils/parser.mjs';
36
import createQueries from '../utils/queries/index.mjs';
47
import { getRemark } from '../utils/remark.mjs';
58

9+
// A ReGeX for retrieving Node.js version headers from the CHANGELOG.md
10+
const NODE_VERSIONS_REGEX = /\* \[Node\.js ([0-9.]+)\]\S+ (.*)\r?\n/g;
11+
12+
// A ReGeX for retrieving the list items in the index document
13+
const LIST_ITEM_REGEX = /\* \[(.*?)\]\((.*?)\.md\)/g;
14+
15+
// A ReGeX for checking if a Node.js version is an LTS release
16+
const NODE_LTS_VERSION_REGEX = /Long Term Support/i;
17+
618
/**
719
* Creates an API doc parser for a given Markdown API doc file
820
*
@@ -58,4 +70,35 @@ const createParser = linter => {
5870
return { parseApiDocs, parseApiDoc };
5971
};
6072

73+
/**
74+
* Retrieves all Node.js major versions from the provided CHANGELOG.md file
75+
* and returns an array of objects containing the version and LTS status.
76+
* @param {string|URL} path Path to changelog
77+
* @returns {Promise<Array<ApiDocReleaseEntry>>}
78+
*/
79+
export const parseChangelog = async path => {
80+
const changelog = await loadFromURL(path);
81+
82+
const nodeMajors = Array.from(changelog.matchAll(NODE_VERSIONS_REGEX));
83+
84+
return nodeMajors.map(match => ({
85+
version: coerce(match[1]),
86+
isLts: NODE_LTS_VERSION_REGEX.test(match[2]),
87+
}));
88+
};
89+
90+
/**
91+
* Retrieves all the document titles for sidebar generation.
92+
*
93+
* @param {string|URL} path Path to changelog
94+
* @returns {Promise<Array<{ section: string, api: string }>>}
95+
*/
96+
export const parseIndex = async path => {
97+
const index = await loadFromURL(path);
98+
99+
const items = Array.from(index.matchAll(LIST_ITEM_REGEX));
100+
101+
return items.map(([, section, api]) => ({ section, api }));
102+
};
103+
61104
export default createParser;

src/releases.mjs

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/utils/__tests__/parser.test.mjs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
import assert from 'node:assert/strict';
4+
import { describe, it, mock } from 'node:test';
5+
6+
mock.module('node:fs/promises', {
7+
namedExports: {
8+
readFile: async () => 'file content',
9+
},
10+
});
11+
12+
global.fetch = mock.fn(() =>
13+
Promise.resolve({
14+
text: () => Promise.resolve('fetched content'),
15+
})
16+
);
17+
18+
const { loadFromURL } = await import('../parser.mjs');
19+
20+
describe('loadFromURL', () => {
21+
it('should load content from a file path', async () => {
22+
const result = await loadFromURL('path/to/file.txt');
23+
assert.equal(result, 'file content');
24+
});
25+
26+
it('should load content from a URL', async () => {
27+
const result = await loadFromURL('https://example.com/data');
28+
assert.equal(result, 'fetched content');
29+
});
30+
});

src/utils/parser.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
3+
import { readFile } from 'node:fs/promises';
4+
5+
/**
6+
* Loads content from a URL or file path
7+
* @param {string|URL} url The URL or file path to load
8+
* @returns {Promise<string>} The content as a string
9+
*/
10+
export const loadFromURL = async url => {
11+
const parsedUrl = url instanceof URL ? url : URL.parse(url);
12+
13+
if (!parsedUrl || parsedUrl.protocol === 'file:') {
14+
// Load from file system
15+
return readFile(url, 'utf-8');
16+
} else {
17+
// Load from network
18+
const response = await fetch(parsedUrl);
19+
return response.text();
20+
}
21+
};

0 commit comments

Comments
 (0)