Skip to content

feat(index): support passing an index file #316

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

Merged
merged 1 commit into from
Jun 23, 2025
Merged
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
16 changes: 12 additions & 4 deletions bin/commands/generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { publicGenerators } from '../../src/generators/index.mjs';
import createGenerator from '../../src/generators.mjs';
import createLinter from '../../src/linter/index.mjs';
import { getEnabledRules } from '../../src/linter/utils/rules.mjs';
import createNodeReleases from '../../src/releases.mjs';
import { parseChangelog, parseIndex } from '../../src/parsers/markdown.mjs';
import { loadAndParse } from '../utils.mjs';

const availableGenerators = Object.keys(publicGenerators);
Expand Down Expand Up @@ -107,6 +107,14 @@ export default {
})),
},
},
index: {
flags: ['--index <path>'],
desc: 'The index document, for getting the titles of various API docs',
prompt: {
message: 'Path to doc/api/index.md',
type: 'text',
},
},
skipLint: {
flags: ['--skip-lint'],
desc: 'Skip lint before generate',
Expand Down Expand Up @@ -135,9 +143,8 @@ export default {
process.exit(1);
}

const { getAllMajors } = createNodeReleases(opts.changelog);

const releases = await getAllMajors();
const releases = await parseChangelog(opts.changelog);
const index = opts.index && (await parseIndex(opts.index));

const { runGenerators } = createGenerator(docs);

Expand All @@ -149,6 +156,7 @@ export default {
releases,
gitRef: opts.gitRef,
threads: parseInt(opts.threads, 10),
index,
});
},
};
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
"format": "prettier .",
"format:write": "prettier --write .",
"format:check": "prettier --check .",
"test": "node --test",
"test": "node --test --experimental-test-module-mocks",
"test:coverage": "c8 npm test",
"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",
"test:update-snapshots": "node --test --test-update-snapshots",
"test:watch": "node --test --watch",
"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",
"test:update-snapshots": "node --test --experimental-test-module-mocks --test-update-snapshots",
"test:watch": "node --test --experimental-test-module-mocks --watch",
"prepare": "husky",
"run": "node bin/cli.mjs",
"watch": "node --watch bin/cli.mjs"
Expand Down
11 changes: 9 additions & 2 deletions src/generators/legacy-html/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default {
* @param {Input} input
* @param {Partial<GeneratorOptions>} options
*/
async generate(input, { releases, version, output }) {
async generate(input, { index, releases, version, output }) {
// This array holds all the generated values for each module
const generatedValues = [];

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

const indexOfFiles = index
? index.map(entry => ({
api: entry.api,
heading: { data: { depth: 1, name: entry.section } },
}))
: headNodes;

// Generates the global Table of Contents (Sidebar Navigation)
const parsedSideNav = remarkRehypeProcessor.processSync(
tableOfContents(headNodes, {
tableOfContents(indexOfFiles, {
maxDepth: 1,
parser: tableOfContents.parseNavigationNode,
})
Expand Down
3 changes: 3 additions & 0 deletions src/generators/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ declare global {
// A list of all Node.js major versions and their respective release information
releases: Array<ApiDocReleaseEntry>;

// A list of all the titles of all the documentation files
index: Array<{ section: string; api: string }>;

// An URL containing a git ref URL pointing to the commit or ref that was used
// to generate the API docs. This is used to link to the source code of the
// i.e. https://github.com/nodejs/node/tree/2cb1d07e0f6d9456438016bab7db4688ab354fd2
Expand Down
52 changes: 52 additions & 0 deletions src/parsers/__tests__/markdown.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';

import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';

import dedent from 'dedent';

let content;
mock.module('../../utils/parser.mjs', {
namedExports: {
loadFromURL: async () => content,
},
});

const { parseChangelog, parseIndex } = await import('../markdown.mjs');

describe('parseChangelog', () => {
it('should parse Node.js versions and their LTS status', async () => {
content = dedent`
* [Node.js 24](doc/changelogs/CHANGELOG_V24.md) **Current**
* [Node.js 22](doc/changelogs/CHANGELOG_V22.md) **Long Term Support**\n
`;

const results = await parseChangelog('...');

assert.partialDeepStrictEqual(results, [
{ version: { raw: '24.0.0' }, isLts: false },
{ version: { raw: '22.0.0' }, isLts: true },
]);
});
});

describe('parseIndex', () => {
it('should retrieve document titles for sidebar generation', async () => {
content = dedent`
# API Documentation

* [Assert](assert.md)
* [Buffer](buffer.md)
* [Child Process](child_process.md)
* [Something](not-a-markdown-file)
`;

const results = await parseIndex('...');

assert.deepStrictEqual(results, [
{ section: 'Assert', api: 'assert' },
{ section: 'Buffer', api: 'buffer' },
{ section: 'Child Process', api: 'child_process' },
]);
});
});
43 changes: 43 additions & 0 deletions src/parsers/markdown.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
'use strict';

import { coerce } from 'semver';

import { loadFromURL } from '../utils/parser.mjs';
import createQueries from '../utils/queries/index.mjs';
import { getRemark } from '../utils/remark.mjs';

// A ReGeX for retrieving Node.js version headers from the CHANGELOG.md
const NODE_VERSIONS_REGEX = /\* \[Node\.js ([0-9.]+)\]\S+ (.*)\r?\n/g;

// A ReGeX for retrieving the list items in the index document
const LIST_ITEM_REGEX = /\* \[(.*?)\]\((.*?)\.md\)/g;

// A ReGeX for checking if a Node.js version is an LTS release
const NODE_LTS_VERSION_REGEX = /Long Term Support/i;

/**
* Creates an API doc parser for a given Markdown API doc file
*
Expand Down Expand Up @@ -58,4 +70,35 @@ const createParser = linter => {
return { parseApiDocs, parseApiDoc };
};

/**
* Retrieves all Node.js major versions from the provided CHANGELOG.md file
* and returns an array of objects containing the version and LTS status.
* @param {string|URL} path Path to changelog
* @returns {Promise<Array<ApiDocReleaseEntry>>}
*/
export const parseChangelog = async path => {
const changelog = await loadFromURL(path);

const nodeMajors = Array.from(changelog.matchAll(NODE_VERSIONS_REGEX));

return nodeMajors.map(match => ({
version: coerce(match[1]),
isLts: NODE_LTS_VERSION_REGEX.test(match[2]),
}));
};

/**
* Retrieves all the document titles for sidebar generation.
*
* @param {string|URL} path Path to changelog
* @returns {Promise<Array<{ section: string, api: string }>>}
*/
export const parseIndex = async path => {
const index = await loadFromURL(path);

const items = Array.from(index.matchAll(LIST_ITEM_REGEX));

return items.map(([, section, api]) => ({ section, api }));
};

export default createParser;
65 changes: 0 additions & 65 deletions src/releases.mjs

This file was deleted.

30 changes: 30 additions & 0 deletions src/utils/__tests__/parser.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';

mock.module('node:fs/promises', {
namedExports: {
readFile: async () => 'file content',
},
});

global.fetch = mock.fn(() =>
Promise.resolve({
text: () => Promise.resolve('fetched content'),
})
);

const { loadFromURL } = await import('../parser.mjs');

describe('loadFromURL', () => {
it('should load content from a file path', async () => {
const result = await loadFromURL('path/to/file.txt');
assert.equal(result, 'file content');
});

it('should load content from a URL', async () => {
const result = await loadFromURL('https://example.com/data');
assert.equal(result, 'fetched content');
});
});
21 changes: 21 additions & 0 deletions src/utils/parser.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

import { readFile } from 'node:fs/promises';

/**
* Loads content from a URL or file path
* @param {string|URL} url The URL or file path to load
* @returns {Promise<string>} The content as a string
*/
export const loadFromURL = async url => {
const parsedUrl = url instanceof URL ? url : URL.parse(url);

if (!parsedUrl || parsedUrl.protocol === 'file:') {
// Load from file system
return readFile(url, 'utf-8');
} else {
// Load from network
const response = await fetch(parsedUrl);
return response.text();
}
};
Loading