From 798cc5dda423fc99c596b1ebb540bacae5c2c1da Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:22:15 +0100 Subject: [PATCH 1/6] test: improve test coverage --- .c8rc.json | 1 + .../addon-verify/__tests__/index.test.mjs | 78 ++++++++++++ .../llms-txt/__tests__/index.test.mjs | 58 +++++++++ .../man-page/__tests__/index.test.mjs | 97 ++++++++++++++ .../metadata/__tests__/index.test.mjs | 31 +++++ .../metadata/__tests__/parse.test.mjs | 118 ++++++++++++++++++ .../orama-db/__tests__/generate.test.mjs | 84 +++++++++++++ .../web/utils/__tests__/utils.test.mjs | 76 +++++++++++ 8 files changed, 543 insertions(+) create mode 100644 src/generators/addon-verify/__tests__/index.test.mjs create mode 100644 src/generators/llms-txt/__tests__/index.test.mjs create mode 100644 src/generators/man-page/__tests__/index.test.mjs create mode 100644 src/generators/metadata/__tests__/index.test.mjs create mode 100644 src/generators/metadata/__tests__/parse.test.mjs create mode 100644 src/generators/orama-db/__tests__/generate.test.mjs create mode 100644 src/generators/web/utils/__tests__/utils.test.mjs diff --git a/.c8rc.json b/.c8rc.json index 3c63a3ff..c4cc29c8 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -2,6 +2,7 @@ "all": true, "exclude": [ "eslint.config.mjs", + "**/*.test.mjs", "**/fixtures", "src/generators/legacy-html/assets", "src/generators/web/ui", diff --git a/src/generators/addon-verify/__tests__/index.test.mjs b/src/generators/addon-verify/__tests__/index.test.mjs new file mode 100644 index 00000000..91d2fad7 --- /dev/null +++ b/src/generators/addon-verify/__tests__/index.test.mjs @@ -0,0 +1,78 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, readFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +import { u } from 'unist-builder'; + +import addon from '../index.mjs'; +import { + normalizeSectionName, + generateSectionFolderName, +} from '../utils/section.mjs'; + +test('returns empty array when no code blocks match filename comment', async () => { + const entry = { + heading: { data: { name: 'Section A' } }, + content: u('root', [u('code', 'console.log("no filename header");')]), + }; + + const result = await addon.generate([entry], {}); + + // No sections were buildable / no filenames extracted + assert.deepEqual(result, []); +}); + +test('ignores non-buildable sections (needs both .cc and .js)', async () => { + // Only a .cc file present -> not buildable + const entry = { + heading: { data: { name: 'OnlyCC' } }, + content: u('root', [u('code', '// file1.cc\nint main() {}')]), + }; + + const result = await addon.generate([entry], {}); + + assert.deepEqual(result, []); +}); + +test('generates files array and writes files to disk when output provided', async () => { + const sectionName = 'My Addon Section'; + + const entry = { + heading: { data: { name: sectionName } }, + content: u('root', [ + u('code', '// file1.cc\nint main() {}'), + u( + 'code', + "// test.js\nmodule.exports = require('./build/Release/addon');" + ), + ]), + }; + + const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-')); + + const returned = await addon.generate([entry], { output: tmp }); + + // Returned is an array of file arrays (one per section) + assert.equal(Array.isArray(returned), true); + assert.equal(returned.length, 1); + + const files = returned[0]; + + assert.ok(files.some(f => f.name === 'file1.cc')); + assert.ok(files.some(f => f.name === 'test.js')); + assert.ok(files.some(f => f.name === 'binding.gyp')); + + // Verify files were written to disk under the computed folder name + const folderName = generateSectionFolderName( + normalizeSectionName(sectionName), + 0 + ); + + const file1 = await readFile(join(tmp, folderName, 'file1.cc'), 'utf-8'); + const binding = await readFile(join(tmp, folderName, 'binding.gyp'), 'utf-8'); + + assert.match(file1, /int main/); + assert.match(binding, /targets/); +}); diff --git a/src/generators/llms-txt/__tests__/index.test.mjs b/src/generators/llms-txt/__tests__/index.test.mjs new file mode 100644 index 00000000..250f0559 --- /dev/null +++ b/src/generators/llms-txt/__tests__/index.test.mjs @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict'; +import { readFile, mkdtemp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +import llms from '../index.mjs'; + +const makeEntry = ({ + title = 'MyAPI', + depth = 1, + desc = 'A description', + api = 'doc/some/path.md', + llm_description, +} = {}) => ({ + heading: { depth, data: { name: title } }, + content: { + children: [ + { type: 'paragraph', children: [{ type: 'text', value: desc }] }, + ], + }, + api_doc_source: api, + llm_description, +}); + +test('generate returns filled template including depth 1 entries', async () => { + const entry = makeEntry({ title: 'Alpha', desc: 'Alpha description' }); + + const result = await llms.generate([entry], {}); + + assert.equal(typeof result, 'string'); + assert.match(result, /- \[Alpha\]/); + assert.match(result, /Alpha description/); +}); + +test('generate only includes depth 1 headings', async () => { + const entry1 = makeEntry({ title: 'Top', depth: 1, desc: 'Top desc' }); + const entry2 = makeEntry({ title: 'Sub', depth: 2, desc: 'Sub desc' }); + + const result = await llms.generate([entry1, entry2], {}); + + assert.match(result, /- \[Top\]/); + assert.doesNotMatch(result, /- \[Sub\]/); +}); + +test('generate writes llms.txt when output is provided', async () => { + const entry = makeEntry({ title: 'WriteTest', desc: 'Write description' }); + + const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-')); + + const returned = await llms.generate([entry], { output: tmp }); + + const file = await readFile(join(tmp, 'llms.txt'), 'utf-8'); + + assert.equal(returned, file); + assert.match(file, /- \[WriteTest\]/); + assert.match(file, /Write description/); +}); diff --git a/src/generators/man-page/__tests__/index.test.mjs b/src/generators/man-page/__tests__/index.test.mjs new file mode 100644 index 00000000..850d4773 --- /dev/null +++ b/src/generators/man-page/__tests__/index.test.mjs @@ -0,0 +1,97 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, readFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +import { u } from 'unist-builder'; + +import manpage from '../index.mjs'; + +const textNode = txt => u('text', txt); + +const createMock = ({ + api = 'cli', + slug = '', + depth = 2, + headingText = '', + desc = '', +} = {}) => ({ + api, + slug, + heading: { depth, data: { text: headingText } }, + // eslint-disable-next-line no-sparse-arrays + content: u('root', [, u('paragraph', [textNode(desc)])]), +}); + +test('throws when no cli documentation present', async () => { + await assert.rejects( + async () => { + await manpage.generate([{ api: 'not-cli' }], {}); + }, + { message: /Could not find any `cli` documentation/ } + ); +}); + +test('generates mandoc including options and environment entries', async () => { + const components = [ + createMock({ api: 'cli', slug: 'cli', depth: 1 }), + createMock({ api: 'cli', slug: 'options', depth: 2 }), + createMock({ + api: 'cli', + slug: 'opt-a', + depth: 3, + headingText: '`-a`, `--all`', + desc: 'Option A description', + }), + createMock({ api: 'cli', slug: 'environment-variables-1', depth: 2 }), + createMock({ + api: 'cli', + slug: 'env-foo', + depth: 3, + headingText: '`FOO=bar`', + desc: 'Env FOO description', + }), + createMock({ api: 'cli', slug: 'after', depth: 2 }), + ]; + + const result = await manpage.generate(components, {}); + + // Ensure mandoc markers for options and environment variables are present + assert.match(result, /\.It Fl/); + assert.match(result, /Option A description/); + assert.match(result, /\.It Ev/); + assert.match(result, /Env FOO description/); +}); + +test('writes node.1 to output when provided', async () => { + const components = [ + createMock({ api: 'cli', slug: 'options', depth: 2 }), + createMock({ + api: 'cli', + slug: 'opt-a', + depth: 3, + headingText: '`-a`', + desc: 'desc', + }), + createMock({ api: 'cli', slug: 'environment-variables-1', depth: 2 }), + createMock({ + api: 'cli', + slug: 'env', + depth: 3, + headingText: '`X=`', + desc: 'env desc', + }), + createMock({ api: 'cli', slug: 'end', depth: 2 }), + ]; + + const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-')); + + const returned = await manpage.generate(components, { output: tmp }); + + const file = await readFile(join(tmp, 'node.1'), 'utf-8'); + + assert.equal(returned, file); + assert.match(file, /desc/); + assert.match(file, /env desc/); +}); diff --git a/src/generators/metadata/__tests__/index.test.mjs b/src/generators/metadata/__tests__/index.test.mjs new file mode 100644 index 00000000..ab4494a4 --- /dev/null +++ b/src/generators/metadata/__tests__/index.test.mjs @@ -0,0 +1,31 @@ +import { deepStrictEqual, strictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; + +import generator from '../index.mjs'; + +describe('generators/metadata/index', () => { + it('streams chunk results and yields flattened arrays', async () => { + const inputs = [1, 2, 3]; + + const worker = { + // Simulate an async generator that yields chunked results + async *stream() { + yield [[1, 2], [3]]; + yield [[4]]; + }, + }; + + const results = []; + + for await (const chunk of generator.generate(inputs, { + typeMap: {}, + worker, + })) { + results.push(chunk); + } + + strictEqual(results.length, 2); + deepStrictEqual(results[0], [1, 2, 3]); + deepStrictEqual(results[1], [4]); + }); +}); diff --git a/src/generators/metadata/__tests__/parse.test.mjs b/src/generators/metadata/__tests__/parse.test.mjs new file mode 100644 index 00000000..689df846 --- /dev/null +++ b/src/generators/metadata/__tests__/parse.test.mjs @@ -0,0 +1,118 @@ +import { strictEqual, deepStrictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; + +import { u } from 'unist-builder'; +import { VFile } from 'vfile'; + +import { parseApiDoc } from '../utils/parse.mjs'; + +describe('generators/metadata/utils/parse', () => { + it('parses heading, stability, YAML and converts markdown links', () => { + const tree = u('root', [ + u('heading', { depth: 1 }, [u('text', 'My API')]), + u('blockquote', [u('paragraph', [u('text', 'Stability: 2 - stable')])]), + u('html', ''), + u('paragraph', [ + u('text', 'See '), + u('link', { url: 'other.md#foo' }, [u('text', 'other')]), + ]), + ]); + + const file = new VFile({ path: 'doc/api/my-api.md' }); + + const results = parseApiDoc({ file, tree }, {}); + + strictEqual(results.length, 1); + const [entry] = results; + + strictEqual(entry.source_link, 'https://example.com'); + strictEqual(entry.stability.children.length, 1); + strictEqual(entry.stability.children[0].data.index, '2'); + + // Find a paragraph child that contains a link and assert transformed URL + const paragraph = entry.content.children.find(n => n.type === 'paragraph'); + const link = paragraph.children.find(c => c.type === 'link'); + strictEqual(link.url, 'other.html#foo'); + }); + + it('inserts a fake heading when none exist', () => { + const tree = u('root', [u('paragraph', [u('text', 'No heading content')])]); + const file = new VFile({ path: 'doc/api/noheading.md' }); + + const results = parseApiDoc({ file, tree }, {}); + + strictEqual(results.length, 1); + const [entry] = results; + + // Fake heading has empty text + deepStrictEqual(entry.heading.data.text, ''); + }); + + it('converts link references using definitions and removes definitions', () => { + const heading = u('heading', { depth: 1 }, [u('text', 'Ref API')]); + + const linkRef = u('linkReference', { identifier: 'def1' }, [ + u('text', 'ref'), + ]); + + const definition = u( + 'definition', + { identifier: 'def1', url: 'https://def.example/' }, + [] + ); + + const tree = u('root', [ + heading, + u('paragraph', [u('text', 'See '), linkRef]), + definition, + ]); + + const file = new VFile({ path: 'doc/api/ref-api.md' }); + + const results = parseApiDoc({ file, tree }, {}); + + strictEqual(results.length, 1); + const [entry] = results; + + const paragraph = entry.content.children.find(n => n.type === 'paragraph'); + const link = paragraph.children.find(c => c.type === 'link'); + strictEqual(link.url, 'https://def.example/'); + }); + + it('converts type references to links using provided typeMap', () => { + const tree = u('root', [ + u('heading', { depth: 1 }, [u('text', 'Types API')]), + u('paragraph', [u('text', 'Type is {Foo}')]), + ]); + + const file = new VFile({ path: 'doc/api/types.md' }); + + const results = parseApiDoc({ file, tree }, { Foo: 'foo.html' }); + + strictEqual(results.length, 1); + const [entry] = results; + + const paragraph = entry.content.children.find(n => n.type === 'paragraph'); + const link = paragraph.children.find(c => c.type === 'link'); + strictEqual(link.url, 'foo.html'); + }); + + it('converts unix manual references to man7 links', () => { + const tree = u('root', [ + u('heading', { depth: 1 }, [u('text', 'Man API')]), + u('paragraph', [u('text', 'Run ls(1) for help')]), + ]); + + const file = new VFile({ path: 'doc/api/man.md' }); + + const results = parseApiDoc({ file, tree }, {}); + + strictEqual(results.length, 1); + const [entry] = results; + + const paragraph = entry.content.children.find(n => n.type === 'paragraph'); + const link = paragraph.children.find(c => c.type === 'link'); + // should point to man7 man page for ls in section 1 + strictEqual(link.url.includes('man-pages/man1/ls.1.html'), true); + }); +}); diff --git a/src/generators/orama-db/__tests__/generate.test.mjs b/src/generators/orama-db/__tests__/generate.test.mjs new file mode 100644 index 00000000..d3b16470 --- /dev/null +++ b/src/generators/orama-db/__tests__/generate.test.mjs @@ -0,0 +1,84 @@ +import { strictEqual, rejects, ok } from 'node:assert'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, it } from 'node:test'; + +import generator from '../index.mjs'; + +describe('orama-db generate', () => { + it('throws when input is missing or empty', async () => { + await rejects(async () => + generator.generate(undefined, { output: './tmp' }) + ); + await rejects(async () => generator.generate([], { output: './tmp' })); + }); + + it('throws when output path is missing', async () => { + const fakeInput = [ + { + api: 'a', + slug: 's', + heading: { data: { name: 'A' }, depth: 1 }, + content: { children: [] }, + }, + ]; + await rejects(async () => + generator.generate(fakeInput, { output: undefined }) + ); + }); + + it('writes an orama-db.json file with expected documents', async () => { + // prepare temporary output directory + const dir = await mkdtemp(join(tmpdir(), 'orama-')); + + try { + const input = [ + // Two entries in same module to test grouping and hierarchical titles + { + api: 'mymod', + slug: 'one', + heading: { data: { name: 'Module' }, depth: 1 }, + content: { + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'First description' }], + }, + ], + }, + }, + { + api: 'mymod', + slug: 'two', + heading: { data: { name: 'Child' }, depth: 2 }, + content: { + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Second description' }], + }, + ], + }, + }, + ]; + + await generator.generate(input, { output: dir }); + + const file = await readFile(join(dir, 'orama-db.json'), 'utf8'); + const parsed = JSON.parse(file); + + // Basic sanity checks on saved DB structure + ok(parsed, 'saved DB should be JSON'); + // Expect some representation of documents to exist + // The exact schema is internal to orama; ensure serialized contains our slugs/titles + const serialized = JSON.stringify(parsed); + strictEqual(serialized.includes('mymod.html#one'), true); + strictEqual(serialized.includes('mymod.html#two'), true); + strictEqual(serialized.includes('Module'), true); + strictEqual(serialized.includes('Child'), true); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/src/generators/web/utils/__tests__/utils.test.mjs b/src/generators/web/utils/__tests__/utils.test.mjs new file mode 100644 index 00000000..df1f30ed --- /dev/null +++ b/src/generators/web/utils/__tests__/utils.test.mjs @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { createChunkedRequire } from '../chunks.mjs'; +import cssPlugin from '../css.mjs'; +import createBuilders, { createImportDeclaration } from '../generate.mjs'; + +test('createChunkedRequire resolves virtual chunks and falls back to require', () => { + const chunks = [ + { fileName: 'a.js', code: 'module.exports = { val: 1 };' }, + { + fileName: 'b.js', + code: 'const a = require("./a.js"); module.exports = { val: a.val + 1 };', + }, + ]; + + const fakeRequire = path => { + if (path === 'fs') { + return { read: true }; + } + return null; + }; + + const req = createChunkedRequire(chunks, fakeRequire); + + // resolve virtual module + const a = req('./a.js'); + assert.deepEqual(a, { val: 1 }); + + // module that requires another virtual module + const b = req('./b.js'); + assert.deepEqual(b, { val: 2 }); + + // fallback to external + const ext = req('fs'); + assert.deepEqual(ext, { read: true }); +}); + +test('createImportDeclaration produces correct import strings', () => { + // side-effect import + assert.equal( + createImportDeclaration(null, './style.css'), + 'import "./style.css";' + ); + + // default import + assert.equal(createImportDeclaration('X', './mod'), 'import X from "./mod";'); + + // named import + assert.equal( + createImportDeclaration('Y', './mod', false), + 'import { Y } from "./mod";' + ); +}); + +test('builders produce client and server programs containing expected markers', () => { + const { buildClientProgram, buildServerProgram } = createBuilders(); + + const client = buildClientProgram('MyComp()'); + assert.match(client, /hydrate\(MyComp\(\)/); + assert.match(client, /index\.css/); + + const server = buildServerProgram('MyComp()'); + assert.match(server, /return render\(MyComp\(\)\);/); +}); + +test('css plugin buildEnd is a no-op when no chunks processed', () => { + const plugin = cssPlugin(); + + let emitted = null; + const thisArg = { emitFile: info => (emitted = info) }; + + // Should not throw and should not emit anything + plugin.buildEnd.call(thisArg); + assert.equal(emitted, null); +}); From 7f0264acb4a3165184c55d40123ff1d49030451a Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:32:42 +0100 Subject: [PATCH 2/6] test: use codebase style guide --- .../addon-verify/__tests__/index.test.mjs | 133 ++++++++-------- .../llms-txt/__tests__/index.test.mjs | 48 +++--- .../man-page/__tests__/index.test.mjs | 128 ++++++++-------- .../web/utils/__tests__/utils.test.mjs | 143 +++++++++--------- 4 files changed, 233 insertions(+), 219 deletions(-) diff --git a/src/generators/addon-verify/__tests__/index.test.mjs b/src/generators/addon-verify/__tests__/index.test.mjs index 91d2fad7..8e44fc0f 100644 --- a/src/generators/addon-verify/__tests__/index.test.mjs +++ b/src/generators/addon-verify/__tests__/index.test.mjs @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { mkdtemp, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import test from 'node:test'; +import { describe, it } from 'node:test'; import { u } from 'unist-builder'; @@ -12,67 +12,72 @@ import { generateSectionFolderName, } from '../utils/section.mjs'; -test('returns empty array when no code blocks match filename comment', async () => { - const entry = { - heading: { data: { name: 'Section A' } }, - content: u('root', [u('code', 'console.log("no filename header");')]), - }; - - const result = await addon.generate([entry], {}); - - // No sections were buildable / no filenames extracted - assert.deepEqual(result, []); -}); - -test('ignores non-buildable sections (needs both .cc and .js)', async () => { - // Only a .cc file present -> not buildable - const entry = { - heading: { data: { name: 'OnlyCC' } }, - content: u('root', [u('code', '// file1.cc\nint main() {}')]), - }; - - const result = await addon.generate([entry], {}); - - assert.deepEqual(result, []); -}); - -test('generates files array and writes files to disk when output provided', async () => { - const sectionName = 'My Addon Section'; - - const entry = { - heading: { data: { name: sectionName } }, - content: u('root', [ - u('code', '// file1.cc\nint main() {}'), - u( - 'code', - "// test.js\nmodule.exports = require('./build/Release/addon');" - ), - ]), - }; - - const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-')); - - const returned = await addon.generate([entry], { output: tmp }); - - // Returned is an array of file arrays (one per section) - assert.equal(Array.isArray(returned), true); - assert.equal(returned.length, 1); - - const files = returned[0]; - - assert.ok(files.some(f => f.name === 'file1.cc')); - assert.ok(files.some(f => f.name === 'test.js')); - assert.ok(files.some(f => f.name === 'binding.gyp')); - - // Verify files were written to disk under the computed folder name - const folderName = generateSectionFolderName( - normalizeSectionName(sectionName), - 0 - ); - - const file1 = await readFile(join(tmp, folderName, 'file1.cc'), 'utf-8'); - const binding = await readFile(join(tmp, folderName, 'binding.gyp'), 'utf-8'); - - assert.match(file1, /int main/); - assert.match(binding, /targets/); +describe('generators/addon-verify', () => { + it('returns empty array when no code blocks match filename comment', async () => { + const entry = { + heading: { data: { name: 'Section A' } }, + content: u('root', [u('code', 'console.log("no filename header");')]), + }; + + const result = await addon.generate([entry], {}); + + // No sections were buildable / no filenames extracted + assert.deepEqual(result, []); + }); + + it('ignores non-buildable sections (needs both .cc and .js)', async () => { + // Only a .cc file present -> not buildable + const entry = { + heading: { data: { name: 'OnlyCC' } }, + content: u('root', [u('code', '// file1.cc\nint main() {}')]), + }; + + const result = await addon.generate([entry], {}); + + assert.deepEqual(result, []); + }); + + it('generates files array and writes files to disk when output provided', async () => { + const sectionName = 'My Addon Section'; + + const entry = { + heading: { data: { name: sectionName } }, + content: u('root', [ + u('code', '// file1.cc\nint main() {}'), + u( + 'code', + "// test.js\nmodule.exports = require('./build/Release/addon');" + ), + ]), + }; + + const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-')); + + const returned = await addon.generate([entry], { output: tmp }); + + // Returned is an array of file arrays (one per section) + assert.equal(Array.isArray(returned), true); + assert.equal(returned.length, 1); + + const files = returned[0]; + + assert.ok(files.some(f => f.name === 'file1.cc')); + assert.ok(files.some(f => f.name === 'test.js')); + assert.ok(files.some(f => f.name === 'binding.gyp')); + + // Verify files were written to disk under the computed folder name + const folderName = generateSectionFolderName( + normalizeSectionName(sectionName), + 0 + ); + + const file1 = await readFile(join(tmp, folderName, 'file1.cc'), 'utf-8'); + const binding = await readFile( + join(tmp, folderName, 'binding.gyp'), + 'utf-8' + ); + + assert.match(file1, /int main/); + assert.match(binding, /targets/); + }); }); diff --git a/src/generators/llms-txt/__tests__/index.test.mjs b/src/generators/llms-txt/__tests__/index.test.mjs index 250f0559..8f2771ad 100644 --- a/src/generators/llms-txt/__tests__/index.test.mjs +++ b/src/generators/llms-txt/__tests__/index.test.mjs @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { readFile, mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import test from 'node:test'; +import { describe, it } from 'node:test'; import llms from '../index.mjs'; @@ -23,36 +23,38 @@ const makeEntry = ({ llm_description, }); -test('generate returns filled template including depth 1 entries', async () => { - const entry = makeEntry({ title: 'Alpha', desc: 'Alpha description' }); +describe('generators/llms-txt', () => { + it('returns filled template including depth 1 entries', async () => { + const entry = makeEntry({ title: 'Alpha', desc: 'Alpha description' }); - const result = await llms.generate([entry], {}); + const result = await llms.generate([entry], {}); - assert.equal(typeof result, 'string'); - assert.match(result, /- \[Alpha\]/); - assert.match(result, /Alpha description/); -}); + assert.equal(typeof result, 'string'); + assert.match(result, /- \[Alpha\]/); + assert.match(result, /Alpha description/); + }); -test('generate only includes depth 1 headings', async () => { - const entry1 = makeEntry({ title: 'Top', depth: 1, desc: 'Top desc' }); - const entry2 = makeEntry({ title: 'Sub', depth: 2, desc: 'Sub desc' }); + it('only includes depth 1 headings', async () => { + const entry1 = makeEntry({ title: 'Top', depth: 1, desc: 'Top desc' }); + const entry2 = makeEntry({ title: 'Sub', depth: 2, desc: 'Sub desc' }); - const result = await llms.generate([entry1, entry2], {}); + const result = await llms.generate([entry1, entry2], {}); - assert.match(result, /- \[Top\]/); - assert.doesNotMatch(result, /- \[Sub\]/); -}); + assert.match(result, /- \[Top\]/); + assert.doesNotMatch(result, /- \[Sub\]/); + }); -test('generate writes llms.txt when output is provided', async () => { - const entry = makeEntry({ title: 'WriteTest', desc: 'Write description' }); + it('writes llms.txt when output is provided', async () => { + const entry = makeEntry({ title: 'WriteTest', desc: 'Write description' }); - const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-')); + const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-')); - const returned = await llms.generate([entry], { output: tmp }); + const returned = await llms.generate([entry], { output: tmp }); - const file = await readFile(join(tmp, 'llms.txt'), 'utf-8'); + const file = await readFile(join(tmp, 'llms.txt'), 'utf-8'); - assert.equal(returned, file); - assert.match(file, /- \[WriteTest\]/); - assert.match(file, /Write description/); + assert.equal(returned, file); + assert.match(file, /- \[WriteTest\]/); + assert.match(file, /Write description/); + }); }); diff --git a/src/generators/man-page/__tests__/index.test.mjs b/src/generators/man-page/__tests__/index.test.mjs index 850d4773..7acdc5fd 100644 --- a/src/generators/man-page/__tests__/index.test.mjs +++ b/src/generators/man-page/__tests__/index.test.mjs @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { mkdtemp, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import test from 'node:test'; +import { describe, it } from 'node:test'; import { u } from 'unist-builder'; @@ -24,74 +24,76 @@ const createMock = ({ content: u('root', [, u('paragraph', [textNode(desc)])]), }); -test('throws when no cli documentation present', async () => { - await assert.rejects( - async () => { - await manpage.generate([{ api: 'not-cli' }], {}); - }, - { message: /Could not find any `cli` documentation/ } - ); -}); +describe('generators/man-page', () => { + it('throws when no cli documentation present', async () => { + await assert.rejects( + async () => { + await manpage.generate([{ api: 'not-cli' }], {}); + }, + { message: /Could not find any `cli` documentation/ } + ); + }); -test('generates mandoc including options and environment entries', async () => { - const components = [ - createMock({ api: 'cli', slug: 'cli', depth: 1 }), - createMock({ api: 'cli', slug: 'options', depth: 2 }), - createMock({ - api: 'cli', - slug: 'opt-a', - depth: 3, - headingText: '`-a`, `--all`', - desc: 'Option A description', - }), - createMock({ api: 'cli', slug: 'environment-variables-1', depth: 2 }), - createMock({ - api: 'cli', - slug: 'env-foo', - depth: 3, - headingText: '`FOO=bar`', - desc: 'Env FOO description', - }), - createMock({ api: 'cli', slug: 'after', depth: 2 }), - ]; + it('generates mandoc including options and environment entries', async () => { + const components = [ + createMock({ api: 'cli', slug: 'cli', depth: 1 }), + createMock({ api: 'cli', slug: 'options', depth: 2 }), + createMock({ + api: 'cli', + slug: 'opt-a', + depth: 3, + headingText: '`-a`, `--all`', + desc: 'Option A description', + }), + createMock({ api: 'cli', slug: 'environment-variables-1', depth: 2 }), + createMock({ + api: 'cli', + slug: 'env-foo', + depth: 3, + headingText: '`FOO=bar`', + desc: 'Env FOO description', + }), + createMock({ api: 'cli', slug: 'after', depth: 2 }), + ]; - const result = await manpage.generate(components, {}); + const result = await manpage.generate(components, {}); - // Ensure mandoc markers for options and environment variables are present - assert.match(result, /\.It Fl/); - assert.match(result, /Option A description/); - assert.match(result, /\.It Ev/); - assert.match(result, /Env FOO description/); -}); + // Ensure mandoc markers for options and environment variables are present + assert.match(result, /\.It Fl/); + assert.match(result, /Option A description/); + assert.match(result, /\.It Ev/); + assert.match(result, /Env FOO description/); + }); -test('writes node.1 to output when provided', async () => { - const components = [ - createMock({ api: 'cli', slug: 'options', depth: 2 }), - createMock({ - api: 'cli', - slug: 'opt-a', - depth: 3, - headingText: '`-a`', - desc: 'desc', - }), - createMock({ api: 'cli', slug: 'environment-variables-1', depth: 2 }), - createMock({ - api: 'cli', - slug: 'env', - depth: 3, - headingText: '`X=`', - desc: 'env desc', - }), - createMock({ api: 'cli', slug: 'end', depth: 2 }), - ]; + it('writes node.1 to output when provided', async () => { + const components = [ + createMock({ api: 'cli', slug: 'options', depth: 2 }), + createMock({ + api: 'cli', + slug: 'opt-a', + depth: 3, + headingText: '`-a`', + desc: 'desc', + }), + createMock({ api: 'cli', slug: 'environment-variables-1', depth: 2 }), + createMock({ + api: 'cli', + slug: 'env', + depth: 3, + headingText: '`X=`', + desc: 'env desc', + }), + createMock({ api: 'cli', slug: 'end', depth: 2 }), + ]; - const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-')); + const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-')); - const returned = await manpage.generate(components, { output: tmp }); + const returned = await manpage.generate(components, { output: tmp }); - const file = await readFile(join(tmp, 'node.1'), 'utf-8'); + const file = await readFile(join(tmp, 'node.1'), 'utf-8'); - assert.equal(returned, file); - assert.match(file, /desc/); - assert.match(file, /env desc/); + assert.equal(returned, file); + assert.match(file, /desc/); + assert.match(file, /env desc/); + }); }); diff --git a/src/generators/web/utils/__tests__/utils.test.mjs b/src/generators/web/utils/__tests__/utils.test.mjs index df1f30ed..6b57634b 100644 --- a/src/generators/web/utils/__tests__/utils.test.mjs +++ b/src/generators/web/utils/__tests__/utils.test.mjs @@ -1,76 +1,81 @@ import assert from 'node:assert/strict'; -import test from 'node:test'; +import { describe, it } from 'node:test'; import { createChunkedRequire } from '../chunks.mjs'; import cssPlugin from '../css.mjs'; import createBuilders, { createImportDeclaration } from '../generate.mjs'; -test('createChunkedRequire resolves virtual chunks and falls back to require', () => { - const chunks = [ - { fileName: 'a.js', code: 'module.exports = { val: 1 };' }, - { - fileName: 'b.js', - code: 'const a = require("./a.js"); module.exports = { val: a.val + 1 };', - }, - ]; - - const fakeRequire = path => { - if (path === 'fs') { - return { read: true }; - } - return null; - }; - - const req = createChunkedRequire(chunks, fakeRequire); - - // resolve virtual module - const a = req('./a.js'); - assert.deepEqual(a, { val: 1 }); - - // module that requires another virtual module - const b = req('./b.js'); - assert.deepEqual(b, { val: 2 }); - - // fallback to external - const ext = req('fs'); - assert.deepEqual(ext, { read: true }); -}); - -test('createImportDeclaration produces correct import strings', () => { - // side-effect import - assert.equal( - createImportDeclaration(null, './style.css'), - 'import "./style.css";' - ); - - // default import - assert.equal(createImportDeclaration('X', './mod'), 'import X from "./mod";'); - - // named import - assert.equal( - createImportDeclaration('Y', './mod', false), - 'import { Y } from "./mod";' - ); -}); - -test('builders produce client and server programs containing expected markers', () => { - const { buildClientProgram, buildServerProgram } = createBuilders(); - - const client = buildClientProgram('MyComp()'); - assert.match(client, /hydrate\(MyComp\(\)/); - assert.match(client, /index\.css/); - - const server = buildServerProgram('MyComp()'); - assert.match(server, /return render\(MyComp\(\)\);/); -}); - -test('css plugin buildEnd is a no-op when no chunks processed', () => { - const plugin = cssPlugin(); - - let emitted = null; - const thisArg = { emitFile: info => (emitted = info) }; - - // Should not throw and should not emit anything - plugin.buildEnd.call(thisArg); - assert.equal(emitted, null); +describe('generators/web/utils', () => { + it('createChunkedRequire resolves virtual chunks and falls back to require', () => { + const chunks = [ + { fileName: 'a.js', code: 'module.exports = { val: 1 };' }, + { + fileName: 'b.js', + code: 'const a = require("./a.js"); module.exports = { val: a.val + 1 };', + }, + ]; + + const fakeRequire = path => { + if (path === 'fs') { + return { read: true }; + } + return null; + }; + + const req = createChunkedRequire(chunks, fakeRequire); + + // resolve virtual module + const a = req('./a.js'); + assert.deepEqual(a, { val: 1 }); + + // module that requires another virtual module + const b = req('./b.js'); + assert.deepEqual(b, { val: 2 }); + + // fallback to external + const ext = req('fs'); + assert.deepEqual(ext, { read: true }); + }); + + it('createImportDeclaration produces correct import strings', () => { + // side-effect import + assert.equal( + createImportDeclaration(null, './style.css'), + 'import "./style.css";' + ); + + // default import + assert.equal( + createImportDeclaration('X', './mod'), + 'import X from "./mod";' + ); + + // named import + assert.equal( + createImportDeclaration('Y', './mod', false), + 'import { Y } from "./mod";' + ); + }); + + it('builders produce client and server programs containing expected markers', () => { + const { buildClientProgram, buildServerProgram } = createBuilders(); + + const client = buildClientProgram('MyComp()'); + assert.match(client, /hydrate\(MyComp\(\)/); + assert.match(client, /index\.css/); + + const server = buildServerProgram('MyComp()'); + assert.match(server, /return render\(MyComp\(\)\);/); + }); + + it('css plugin buildEnd is a no-op when no chunks processed', () => { + const plugin = cssPlugin(); + + let emitted = null; + const thisArg = { emitFile: info => (emitted = info) }; + + // Should not throw and should not emit anything + plugin.buildEnd.call(thisArg); + assert.equal(emitted, null); + }); }); From f7adbc0ebf72517aef25c52d03b1262362c5282f Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:09:01 +0100 Subject: [PATCH 3/6] WIP Co-Authored-By: Aviv Keller --- .c8rc.json | 1 + .../{ => utils}/__tests__/parse.test.mjs | 6 +- .../web/__tests__/index-generate.test.mjs | 39 ++++++ src/generators/web/index.mjs | 8 +- .../web/utils/__tests__/bundle.test.mjs | 25 ++++ .../web/utils/__tests__/chunks.test.mjs | 37 ++++++ .../web/utils/__tests__/css.test.mjs | 52 ++++++++ .../web/utils/__tests__/generate.test.mjs | 37 ++++++ .../__tests__/processing-injectable.test.mjs | 81 ++++++++++++ .../utils/__tests__/processing-more.test.mjs | 120 ++++++++++++++++++ .../web/utils/__tests__/processing.test.mjs | 7 + .../web/utils/__tests__/utils.test.mjs | 81 ------------ src/generators/web/utils/processing.mjs | 91 +++++++++++-- src/parsers/__tests__/javascript.test.mjs | 15 +++ src/parsers/__tests__/json.test.mjs | 22 ++++ src/utils/__tests__/highlighter.test.mjs | 58 +++++++++ src/utils/__tests__/parser.load.test.mjs | 28 ++++ 17 files changed, 611 insertions(+), 97 deletions(-) rename src/generators/metadata/{ => utils}/__tests__/parse.test.mjs (95%) create mode 100644 src/generators/web/__tests__/index-generate.test.mjs create mode 100644 src/generators/web/utils/__tests__/bundle.test.mjs create mode 100644 src/generators/web/utils/__tests__/chunks.test.mjs create mode 100644 src/generators/web/utils/__tests__/css.test.mjs create mode 100644 src/generators/web/utils/__tests__/generate.test.mjs create mode 100644 src/generators/web/utils/__tests__/processing-injectable.test.mjs create mode 100644 src/generators/web/utils/__tests__/processing-more.test.mjs create mode 100644 src/generators/web/utils/__tests__/processing.test.mjs delete mode 100644 src/generators/web/utils/__tests__/utils.test.mjs create mode 100644 src/parsers/__tests__/javascript.test.mjs create mode 100644 src/parsers/__tests__/json.test.mjs create mode 100644 src/utils/__tests__/highlighter.test.mjs create mode 100644 src/utils/__tests__/parser.load.test.mjs diff --git a/.c8rc.json b/.c8rc.json index c4cc29c8..6f36280e 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -2,6 +2,7 @@ "all": true, "exclude": [ "eslint.config.mjs", + "**/legacy-html/**", "**/*.test.mjs", "**/fixtures", "src/generators/legacy-html/assets", diff --git a/src/generators/metadata/__tests__/parse.test.mjs b/src/generators/metadata/utils/__tests__/parse.test.mjs similarity index 95% rename from src/generators/metadata/__tests__/parse.test.mjs rename to src/generators/metadata/utils/__tests__/parse.test.mjs index 689df846..b2701cca 100644 --- a/src/generators/metadata/__tests__/parse.test.mjs +++ b/src/generators/metadata/utils/__tests__/parse.test.mjs @@ -1,12 +1,12 @@ -import { strictEqual, deepStrictEqual } from 'node:assert'; +import { deepStrictEqual, strictEqual } from 'node:assert/strict'; import { describe, it } from 'node:test'; import { u } from 'unist-builder'; import { VFile } from 'vfile'; -import { parseApiDoc } from '../utils/parse.mjs'; +import { parseApiDoc } from '../parse.mjs'; -describe('generators/metadata/utils/parse', () => { +describe('parseApiDoc', () => { it('parses heading, stability, YAML and converts markdown links', () => { const tree = u('root', [ u('heading', { depth: 1 }, [u('text', 'My API')]), diff --git a/src/generators/web/__tests__/index-generate.test.mjs b/src/generators/web/__tests__/index-generate.test.mjs new file mode 100644 index 00000000..6a720fc5 --- /dev/null +++ b/src/generators/web/__tests__/index-generate.test.mjs @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, readFile } from 'node:fs/promises'; +import os from 'node:os'; +import { join } from 'node:path'; +import { describe, it } from 'node:test'; + +import webGen from '../index.mjs'; + +describe('generators/web - index generate', () => { + it('writes files when output is provided', async () => { + const tmp = await mkdtemp(join(os.tmpdir(), 'doc-kit-test-')); + + const results = [{ html: Buffer.from('
ok
'), api: 'api' }]; + const css = 'body{}'; + const chunks = [{ fileName: 'chunk.js', code: 'console.log(1)' }]; + + const stubProcess = async () => ({ results, css, chunks }); + + const out = await webGen.generate([], { + output: tmp, + version: { version: '1.2.3' }, + overrides: { processJSXEntries: stubProcess }, + }); + + // returns results as strings + css + assert.equal(out.length, 1); + const htmlPath = join(tmp, 'api.html'); + const written = await readFile(htmlPath, 'utf-8'); + assert.ok(written.includes('
ok
')); + + const chunkPath = join(tmp, 'chunk.js'); + const chunkContent = await readFile(chunkPath, 'utf-8'); + assert.equal(chunkContent, 'console.log(1)'); + + const cssPath = join(tmp, 'styles.css'); + const cssContent = await readFile(cssPath, 'utf-8'); + assert.equal(cssContent, css); + }); +}); diff --git a/src/generators/web/index.mjs b/src/generators/web/index.mjs index eb3c654a..cedc585b 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -37,7 +37,7 @@ export default { * @param {Partial} options - Generator options. * @returns {Promise} Processed HTML/CSS/JS content. */ - async generate(input, { output, version }) { + async generate(input, { output, version, overrides } = {}) { const template = await readFile( new URL('template.html', import.meta.url), 'utf-8' @@ -50,12 +50,14 @@ export default { const requireFn = createRequire(import.meta.url); // Process all entries: convert JSX to HTML/CSS/JS - const { results, css, chunks } = await processJSXEntries( + const processFn = overrides?.processJSXEntries || processJSXEntries; + + const { results, css, chunks } = await processFn( input, template, astBuilders, requireFn, - { version } + { version, overrides } ); // Process all entries together (required for code-split bundles) diff --git a/src/generators/web/utils/__tests__/bundle.test.mjs b/src/generators/web/utils/__tests__/bundle.test.mjs new file mode 100644 index 00000000..2d7d8754 --- /dev/null +++ b/src/generators/web/utils/__tests__/bundle.test.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +describe('generators/web/utils - bundle', () => { + it('bundleCode separates assets and chunks and returns expected shape', async () => { + const bundleCode = (await import('../bundle.mjs')).default; + + const codeMap = new Map([['a.js', 'export default 1;']]); + + const result = await bundleCode(codeMap, { server: false }); + + // Basic shape assertions to keep this test hermetic without module mocking + assert.equal(typeof result.css, 'string'); + assert.ok(Array.isArray(result.chunks)); + assert.equal( + typeof result.importMap === 'string' || result.importMap === undefined, + true + ); + assert.ok( + result.chunks.every( + c => typeof c.fileName === 'string' && 'code' in c && 'isEntry' in c + ) + ); + }); +}); diff --git a/src/generators/web/utils/__tests__/chunks.test.mjs b/src/generators/web/utils/__tests__/chunks.test.mjs new file mode 100644 index 00000000..9a18a040 --- /dev/null +++ b/src/generators/web/utils/__tests__/chunks.test.mjs @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { createChunkedRequire } from '../chunks.mjs'; + +describe('generators/web/utils - chunks', () => { + it('createChunkedRequire resolves virtual chunks and falls back to require', () => { + const chunks = [ + { fileName: 'a.js', code: 'module.exports = { val: 1 };' }, + { + fileName: 'b.js', + code: 'const a = require("./a.js"); module.exports = { val: a.val + 1 };', + }, + ]; + + const fakeRequire = path => { + if (path === 'fs') { + return { read: true }; + } + return null; + }; + + const req = createChunkedRequire(chunks, fakeRequire); + + // resolve virtual module + const a = req('./a.js'); + assert.deepEqual(a, { val: 1 }); + + // module that requires another virtual module + const b = req('./b.js'); + assert.deepEqual(b, { val: 2 }); + + // fallback to external + const ext = req('fs'); + assert.deepEqual(ext, { read: true }); + }); +}); diff --git a/src/generators/web/utils/__tests__/css.test.mjs b/src/generators/web/utils/__tests__/css.test.mjs new file mode 100644 index 00000000..32ec72b6 --- /dev/null +++ b/src/generators/web/utils/__tests__/css.test.mjs @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import cssPlugin from '../css.mjs'; + +describe('generators/web/utils - css', () => { + it('css plugin buildEnd is a no-op when no chunks processed', () => { + const plugin = cssPlugin(); + + let emitted = null; + const thisArg = { emitFile: info => (emitted = info) }; + + // Should not throw and should not emit anything + plugin.buildEnd.call(thisArg); + assert.equal(emitted, null); + }); + + it('css plugin processes .module.css files and emits styles.css asset', async () => { + const plugin = cssPlugin(); + + // create temp .module.css file + const { writeFile, unlink } = await import('node:fs/promises'); + const { tmpdir } = await import('node:os'); + const { join } = await import('node:path'); + + const id = join( + tmpdir(), + `doc-kit-test-${Date.now()}-${Math.random()}.module.css` + ); + await writeFile(id, '.btn { color: red; }', 'utf8'); + + // Call the handler to process the css file + const { handler } = plugin.load; + const result = await handler(id); + + // Should return a JS module exporting the mapped class names + assert.match(result.code, /export default/); + assert.match(result.code, /"btn"/); + + // buildEnd should emit a styles.css asset containing the compiled CSS + let emitted = null; + const thisArg = { emitFile: info => (emitted = info) }; + plugin.buildEnd.call(thisArg); + + assert.ok(emitted, 'expected styles.css to be emitted'); + assert.equal(emitted.name, 'styles.css'); + assert.match(String(emitted.source), /color:\s*red/); + + // cleanup + await unlink(id); + }); +}); diff --git a/src/generators/web/utils/__tests__/generate.test.mjs b/src/generators/web/utils/__tests__/generate.test.mjs new file mode 100644 index 00000000..82c1bf1a --- /dev/null +++ b/src/generators/web/utils/__tests__/generate.test.mjs @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import createBuilders, { createImportDeclaration } from '../generate.mjs'; + +describe('generators/web/utils - generate', () => { + it('createImportDeclaration produces correct import strings', () => { + // side-effect import + assert.equal( + createImportDeclaration(null, './style.css'), + 'import "./style.css";' + ); + + // default import + assert.equal( + createImportDeclaration('X', './mod'), + 'import X from "./mod";' + ); + + // named import + assert.equal( + createImportDeclaration('Y', './mod', false), + 'import { Y } from "./mod";' + ); + }); + + it('builders produce client and server programs containing expected markers', () => { + const { buildClientProgram, buildServerProgram } = createBuilders(); + + const client = buildClientProgram('MyComp()'); + assert.match(client, /hydrate\(MyComp\(\)/); + assert.match(client, /index\.css/); + + const server = buildServerProgram('MyComp()'); + assert.match(server, /return render\(MyComp\(\)\);/); + }); +}); diff --git a/src/generators/web/utils/__tests__/processing-injectable.test.mjs b/src/generators/web/utils/__tests__/processing-injectable.test.mjs new file mode 100644 index 00000000..42ebc1bf --- /dev/null +++ b/src/generators/web/utils/__tests__/processing-injectable.test.mjs @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { executeServerCode, processWithCodeMaps } from '../processing.mjs'; + +describe('generators/web/utils - processing (injectable)', () => { + it('executeServerCode accepts injected bundler and chunked require', async () => { + const serverCodeMap = new Map([[`api.jsx`, 'ignored']]); + + const fakeRequire = () => ({ fs: true }); + + const mockBundle = async () => ({ + chunks: [ + { fileName: 'api.js', isEntry: true, code: 'return "

ok

";' }, + ], + css: 'body{}', + }); + + const { pages, css } = await executeServerCode(serverCodeMap, fakeRequire, { + bundleCode: mockBundle, + createChunkedRequire: (chunks, req) => req, + }); + + assert.equal(pages.get('api.js'), '

ok

'); + assert.equal(css, 'body{}'); + }); + + it('processWithCodeMaps builds final HTML and css using injected bundlers', async () => { + const serverCodeMap = new Map([[`api.jsx`, 'ignored']]); + const clientCodeMap = new Map([[`api.jsx`, 'ignored']]); + + const entries = [ + { data: { api: 'api', heading: { data: { name: 'My API' } } } }, + ]; + + const template = + '{{title}}{{dehydrated}}{{importMap}}{{entrypoint}}{{speculationRules}}'; + + const fakeRequire = () => ({ fs: true }); + + const mockServerBundle = async () => ({ + chunks: [ + { + fileName: 'api.js', + isEntry: true, + code: 'return "
server
";', + }, + ], + css: 's{}', + }); + + const mockClientBundle = async () => ({ + chunks: [{ fileName: 'api.js', isEntry: true, code: '/* client */' }], + css: 'c{}', + importMap: '{}', + }); + + const { results, css } = await processWithCodeMaps( + serverCodeMap, + clientCodeMap, + entries, + template, + fakeRequire, + { version: { version: '1.0.0' } }, + { + bundleCode: async map => + map === serverCodeMap + ? await mockServerBundle() + : await mockClientBundle(), + createChunkedRequire: (chunks, req) => req, + transform: ({ code }) => ({ code: Buffer.from(String(code)) }), + } + ); + + assert.equal(results.length, 1); + const html = results[0].html.toString(); + assert.match(html, /
server<\/div>/); + assert.match(html, /My API/); + assert.ok(String(css).includes('s{}') || String(css).includes('c{}')); + }); +}); diff --git a/src/generators/web/utils/__tests__/processing-more.test.mjs b/src/generators/web/utils/__tests__/processing-more.test.mjs new file mode 100644 index 00000000..0eb0218b --- /dev/null +++ b/src/generators/web/utils/__tests__/processing-more.test.mjs @@ -0,0 +1,120 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { + convertJSXToCode, + executeServerCode, + processJSXEntries, +} from '../processing.mjs'; + +describe('generators/web/utils - processing (more)', () => { + it('convertJSXToCode uses injected toJs and builders', () => { + const entries = [{ data: { api: 'myapi' } }]; + + const toJsMock = () => ({ value: 'RAW_CODE' }); + + const buildServerProgram = code => `SERVER:${code}`; + const buildClientProgram = code => `CLIENT:${code}`; + + const { serverCodeMap, clientCodeMap } = convertJSXToCode( + entries, + { buildServerProgram, buildClientProgram }, + { toJs: toJsMock } + ); + + assert.equal(serverCodeMap.get('myapi.jsx'), 'SERVER:RAW_CODE'); + assert.equal(clientCodeMap.get('myapi.jsx'), 'CLIENT:RAW_CODE'); + }); + + it('executeServerCode resolves otherChunks via chunked require', async () => { + const serverCodeMap = new Map([[`api.jsx`, 'ignored']]); + + const fakeRequire = () => ({ ok: true }); + + const mockBundle = async () => ({ + chunks: [ + { + fileName: 'lib.1.js', + isEntry: false, + code: 'module.exports = "LIB";', + }, + { + fileName: 'api.js', + isEntry: true, + code: 'const lib = require("./lib.1.js"); return lib + "-OK";', + }, + ], + css: 'body{}', + }); + + const { pages, css } = await executeServerCode(serverCodeMap, fakeRequire, { + bundleCode: mockBundle, + createChunkedRequire: (chunks, req) => id => { + if (id === './lib.1.js') { + return 'LIB'; + } + return req(id); + }, + }); + + assert.equal(pages.get('api.js'), 'LIB-OK'); + assert.equal(css, 'body{}'); + }); + + it('processJSXEntries end-to-end with overrides', async () => { + const entries = [ + { data: { api: 'api', heading: { data: { name: 'My API' } } } }, + ]; + + const template = + '{{title}}{{dehydrated}}{{importMap}}{{entrypoint}}{{speculationRules}}'; + + const astBuilders = { + buildServerProgram: code => `SERVER:${code}`, + buildClientProgram: code => `CLIENT:${code}`, + }; + + const fakeRequire = () => ({ fs: true }); + + const serverBundle = { + chunks: [ + { + fileName: 'api.js', + isEntry: true, + code: 'return "
server
";', + }, + ], + css: 's{}', + }; + + const clientBundle = { + chunks: [{ fileName: 'api.js', isEntry: true, code: '/* client */' }], + css: 'c{}', + importMap: '{}', + }; + + const bundleCode = async (map, opts) => + opts && opts.server ? serverBundle : clientBundle; + + const overrides = { + bundleCode, + createChunkedRequire: (chunks, req) => req, + transform: ({ code }) => ({ code: Buffer.from(String(code)) }), + toJs: () => ({ value: '/* generated */' }), + }; + + const { results, css } = await processJSXEntries( + entries, + template, + astBuilders, + fakeRequire, + { version: { version: '1.0.0' }, overrides } + ); + + assert.equal(results.length, 1); + const html = results[0].html.toString(); + assert.match(html, /
server<\/div>/); + assert.match(html, /My API/); + assert.ok(String(css).includes('s{}') || String(css).includes('c{}')); + }); +}); diff --git a/src/generators/web/utils/__tests__/processing.test.mjs b/src/generators/web/utils/__tests__/processing.test.mjs new file mode 100644 index 00000000..e2d60a69 --- /dev/null +++ b/src/generators/web/utils/__tests__/processing.test.mjs @@ -0,0 +1,7 @@ +import { describe } from 'node:test'; + +describe.skip('generators/web/utils - processing (skipped)', () => { + // Tests for `processing.mjs` are omitted because they relied on + // experimental module mocking. They should be re-added using + // a stable approach (e.g., dependency injection or test doubles). +}); diff --git a/src/generators/web/utils/__tests__/utils.test.mjs b/src/generators/web/utils/__tests__/utils.test.mjs deleted file mode 100644 index 6b57634b..00000000 --- a/src/generators/web/utils/__tests__/utils.test.mjs +++ /dev/null @@ -1,81 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import { createChunkedRequire } from '../chunks.mjs'; -import cssPlugin from '../css.mjs'; -import createBuilders, { createImportDeclaration } from '../generate.mjs'; - -describe('generators/web/utils', () => { - it('createChunkedRequire resolves virtual chunks and falls back to require', () => { - const chunks = [ - { fileName: 'a.js', code: 'module.exports = { val: 1 };' }, - { - fileName: 'b.js', - code: 'const a = require("./a.js"); module.exports = { val: a.val + 1 };', - }, - ]; - - const fakeRequire = path => { - if (path === 'fs') { - return { read: true }; - } - return null; - }; - - const req = createChunkedRequire(chunks, fakeRequire); - - // resolve virtual module - const a = req('./a.js'); - assert.deepEqual(a, { val: 1 }); - - // module that requires another virtual module - const b = req('./b.js'); - assert.deepEqual(b, { val: 2 }); - - // fallback to external - const ext = req('fs'); - assert.deepEqual(ext, { read: true }); - }); - - it('createImportDeclaration produces correct import strings', () => { - // side-effect import - assert.equal( - createImportDeclaration(null, './style.css'), - 'import "./style.css";' - ); - - // default import - assert.equal( - createImportDeclaration('X', './mod'), - 'import X from "./mod";' - ); - - // named import - assert.equal( - createImportDeclaration('Y', './mod', false), - 'import { Y } from "./mod";' - ); - }); - - it('builders produce client and server programs containing expected markers', () => { - const { buildClientProgram, buildServerProgram } = createBuilders(); - - const client = buildClientProgram('MyComp()'); - assert.match(client, /hydrate\(MyComp\(\)/); - assert.match(client, /index\.css/); - - const server = buildServerProgram('MyComp()'); - assert.match(server, /return render\(MyComp\(\)\);/); - }); - - it('css plugin buildEnd is a no-op when no chunks processed', () => { - const plugin = cssPlugin(); - - let emitted = null; - const thisArg = { emitFile: info => (emitted = info) }; - - // Should not throw and should not emit anything - plugin.buildEnd.call(thisArg); - assert.equal(emitted, null); - }); -}); diff --git a/src/generators/web/utils/processing.mjs b/src/generators/web/utils/processing.mjs index d694c7cd..f900aae7 100644 --- a/src/generators/web/utils/processing.mjs +++ b/src/generators/web/utils/processing.mjs @@ -15,11 +15,13 @@ import { createChunkedRequire } from './chunks.mjs'; * @param {Array} entries - JSX AST entries * @param {function} buildServerProgram - Wraps code for server execution * @param {function} buildClientProgram - Wraps code for client hydration + * @param overrides * @returns {{serverCodeMap: Map, clientCodeMap: Map}} */ export function convertJSXToCode( entries, - { buildServerProgram, buildClientProgram } + { buildServerProgram, buildClientProgram }, + overrides = {} ) { const serverCodeMap = new Map(); const clientCodeMap = new Map(); @@ -28,7 +30,8 @@ export function convertJSXToCode( const fileName = `${entry.data.api}.jsx`; // Convert AST to JavaScript string with JSX syntax - const { value: code } = toJs(entry, { handlers: jsx }); + const toJsFn = overrides.toJs || toJs; + const { value: code } = toJsFn(entry, { handlers: jsx }); // Prepare code for server-side execution (wrapped for SSR) serverCodeMap.set(fileName, buildServerProgram(code)); @@ -49,19 +52,26 @@ export function convertJSXToCode( * * @param {Map} serverCodeMap - Map of fileName to server-side JavaScript code. * @param {ReturnType} requireFn - Node.js require function for external packages. + * @param overrides * @returns {{ pages: Map, css: string }} An object containing a Map of fileName to dehydrated (server-rendered) HTML content (`pages`), and a string of collected CSS (`css`). */ -export async function executeServerCode(serverCodeMap, requireFn) { +export async function executeServerCode( + serverCodeMap, + requireFn, + overrides = {} +) { const dehydratedMap = new Map(); // Bundle all server-side code, which may produce code-split chunks - const { chunks, css } = await bundleCode(serverCodeMap, { server: true }); + const bundler = overrides.bundleCode || bundleCode; + const { chunks, css } = await bundler(serverCodeMap, { server: true }); const entryChunks = chunks.filter(c => c.isEntry); const otherChunks = chunks.filter(c => !c.isEntry); // Create enhanced require function that can resolve code-split chunks - const enhancedRequire = createChunkedRequire(otherChunks, requireFn); + const createReq = overrides.createChunkedRequire || createChunkedRequire; + const enhancedRequire = createReq(otherChunks, requireFn); // Execute each bundled entry and collect dehydrated HTML results for (const chunk of entryChunks) { @@ -91,19 +101,23 @@ export async function processJSXEntries( template, astBuilders, requireFn, - { version } + options = {} ) { + const { version, overrides } = options; + // Step 1: Convert JSX AST to JavaScript (CPU-intensive, could be parallelized) const { serverCodeMap, clientCodeMap } = convertJSXToCode( entries, - astBuilders + astBuilders, + overrides ); // Step 2: Bundle server and client code IN PARALLEL // Both need all entries for code-splitting, but are independent of each other + const bundler = overrides?.bundleCode || bundleCode; const [serverBundle, clientBundle] = await Promise.all([ - executeServerCode(serverCodeMap, requireFn), - bundleCode(clientCodeMap), + executeServerCode(serverCodeMap, requireFn, overrides), + bundler(clientCodeMap), ]); const titleSuffix = `Node.js v${version.version} Documentation`; @@ -127,7 +141,64 @@ export async function processJSXEntries( return { html: finalHTMLBuffer, api }; }); - const { code: minifiedCSS } = transform({ + const cssTransformer = overrides?.transform || transform; + const { code: minifiedCSS } = cssTransformer({ + code: Buffer.from(`${serverBundle.css}\n${clientBundle.css}`), + minify: true, + }); + + return { results, chunks: clientBundle.chunks, css: minifiedCSS }; +} + +/** + * Lower-level helper that accepts pre-built server and client code maps. + * This allows tests to bypass AST -> code conversion and inject controlled + * bundles or bundlers via the `overrides` parameter. + * @param serverCodeMap + * @param clientCodeMap + * @param entries + * @param template + * @param requireFn + * @param root0 + * @param root0.version + * @param overrides + */ +export async function processWithCodeMaps( + serverCodeMap, + clientCodeMap, + entries, + template, + requireFn, + { version } = {}, + overrides = {} +) { + const bundler = overrides?.bundleCode || bundleCode; + + const [serverBundle, clientBundle] = await Promise.all([ + executeServerCode(serverCodeMap, requireFn, overrides), + bundler(clientCodeMap), + ]); + + const titleSuffix = `Node.js v${version.version} Documentation`; + const speculationRulesString = JSON.stringify(SPECULATION_RULES, null, 2); + + const results = entries.map(({ data: { api, heading } }) => { + const fileName = `${api}.js`; + + const renderedHtml = template + .replace('{{title}}', `${heading.data.name} | ${titleSuffix}`) + .replace('{{dehydrated}}', serverBundle.pages.get(fileName) ?? '') + .replace('{{importMap}}', clientBundle.importMap ?? '') + .replace('{{entrypoint}}', `./${fileName}?${randomUUID()}`) + .replace('{{speculationRules}}', speculationRulesString); + + const finalHTMLBuffer = HTMLMinifier.minify(Buffer.from(renderedHtml), {}); + + return { html: finalHTMLBuffer, api }; + }); + + const cssTransformer = overrides?.transform || transform; + const { code: minifiedCSS } = cssTransformer({ code: Buffer.from(`${serverBundle.css}\n${clientBundle.css}`), minify: true, }); diff --git a/src/parsers/__tests__/javascript.test.mjs b/src/parsers/__tests__/javascript.test.mjs new file mode 100644 index 00000000..b5f47091 --- /dev/null +++ b/src/parsers/__tests__/javascript.test.mjs @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { parseJsSource } from '../javascript.mjs'; + +describe('parsers/javascript', () => { + it('parseJsSource throws on non-string value', async () => { + await assert.rejects(() => parseJsSource({ value: 1 })); + }); + + it('parseJsSource returns path on success', async () => { + const res = await parseJsSource({ value: 'return 1;', path: 'p' }); + assert.equal(res.path, 'p'); + }); +}); diff --git a/src/parsers/__tests__/json.test.mjs b/src/parsers/__tests__/json.test.mjs new file mode 100644 index 00000000..d4623875 --- /dev/null +++ b/src/parsers/__tests__/json.test.mjs @@ -0,0 +1,22 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import { join } from 'node:path'; +import { describe, it } from 'node:test'; + +import { parseTypeMap } from '../json.mjs'; + +describe('parsers/json', () => { + it('parseTypeMap returns empty for empty path', async () => { + const obj = await parseTypeMap(''); + assert.deepEqual(obj, {}); + }); + + it('parseTypeMap parses a JSON file', async () => { + const tmp = await mkdtemp(join(os.tmpdir(), 'doc-kit-')); + const file = join(tmp, 'map.json'); + await writeFile(file, JSON.stringify({ a: 'b' })); + const parsed = await parseTypeMap(file); + assert.deepEqual(parsed, { a: 'b' }); + }); +}); diff --git a/src/utils/__tests__/highlighter.test.mjs b/src/utils/__tests__/highlighter.test.mjs new file mode 100644 index 00000000..9c00ce40 --- /dev/null +++ b/src/utils/__tests__/highlighter.test.mjs @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import rehypeShikiji from '../highlighter.mjs'; + +describe('utils - highlighter', () => { + it('handles pre elements with missing code child gracefully', () => { + const tree = { + type: 'root', + children: [ + { + type: 'element', + tagName: 'pre', + children: [{ type: 'text', value: 'no code' }], + }, + ], + }; + + const plugin = rehypeShikiji(); + // should not throw + plugin(tree); + }); + + it('creates switchable code tab for two code blocks', () => { + const code1 = { type: 'element', tagName: 'code', properties: {} }; + const pre1 = { + type: 'element', + tagName: 'pre', + children: [code1], + properties: { class: 'language-cjs', style: 's1' }, + }; + + const code2 = { type: 'element', tagName: 'code', properties: {} }; + const pre2 = { + type: 'element', + tagName: 'pre', + children: [code2], + properties: { class: 'language-mjs', style: 's2' }, + }; + + const tree = { type: 'root', children: [pre1, pre2] }; + + const plugin = rehypeShikiji(); + plugin(tree); + + // first child should be replaced with a pre element (switchable container) + const first = tree.children[0]; + assert.equal(first.tagName, 'pre'); + const hasShikiClass = + (first.properties && + typeof first.properties.class === 'string' && + String(first.properties.class).includes('shiki')) || + (first.properties && + Array.isArray(first.properties.className) && + first.properties.className.includes('shiki')); + assert.ok(hasShikiClass); + }); +}); diff --git a/src/utils/__tests__/parser.load.test.mjs b/src/utils/__tests__/parser.load.test.mjs new file mode 100644 index 00000000..f144bbb7 --- /dev/null +++ b/src/utils/__tests__/parser.load.test.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import { join } from 'node:path'; +import { describe, it } from 'node:test'; + +import { loadFromURL } from '../parser.mjs'; + +describe('utils/parser.loadFromURL', () => { + it('loads from filesystem path', async () => { + const tmp = await mkdtemp(join(os.tmpdir(), 'doc-kit-')); + const file = join(tmp, 'f.txt'); + await writeFile(file, 'hello'); + const content = await loadFromURL(file); + assert.equal(content, 'hello'); + }); + + it('uses fetch for network URLs', async () => { + // stub global.fetch + const orig = global.fetch; + global.fetch = async () => ({ text: async () => 'net' }); + + const content = await loadFromURL(new URL('https://example.test/')); + assert.equal(content, 'net'); + + global.fetch = orig; + }); +}); From 28d73655f49ffdb45fe1694bab0b47b05f6c53c9 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:14:11 +0100 Subject: [PATCH 4/6] WIP --- .c8rc.json | 1 - bin/__tests__/cli.test.mjs | 79 +++++ bin/__tests__/utils.test.mjs | 44 +++ bin/cli.mjs | 81 +++-- bin/commands/__tests__/generate.test.mjs | 87 ++++++ .../__tests__/interactive.branches.test.mjs | 158 ++++++++++ bin/commands/__tests__/interactive.test.mjs | 141 +++++++++ bin/commands/generate.mjs | 14 +- .../utils/__tests__/extractExports.test.mjs | 68 +++++ src/generators/ast/__tests__/index.test.mjs | 56 ++++ .../json-simple/__tests__/index.test.mjs | 67 +++++ .../jsx-ast/__tests__/index.test.mjs | 125 ++++++++ .../utils/__tests__/buildContent.test.mjs | 276 ++++++++++++++++++ .../__tests__/buildPropertyTable.test.mjs | 197 +++++++++++-- .../utils/__tests__/buildSignature.test.mjs | 137 +++++++++ .../__tests__/getSortedHeadNodes.test.mjs | 23 ++ .../utils/__tests__/transformer.test.mjs | 128 ++++++++ .../utils/__tests__/processing-more.test.mjs | 120 -------- .../web/utils/__tests__/processing.test.mjs | 123 +++++++- src/logger/__tests__/index.smoke.test.mjs | 18 ++ src/logger/__tests__/index.test.mjs | 36 +++ src/utils/__tests__/highlighter.test.mjs | 101 +++++-- 22 files changed, 1879 insertions(+), 201 deletions(-) create mode 100644 bin/__tests__/cli.test.mjs create mode 100644 bin/__tests__/utils.test.mjs create mode 100644 bin/commands/__tests__/generate.test.mjs create mode 100644 bin/commands/__tests__/interactive.branches.test.mjs create mode 100644 bin/commands/__tests__/interactive.test.mjs create mode 100644 src/generators/api-links/utils/__tests__/extractExports.test.mjs create mode 100644 src/generators/ast/__tests__/index.test.mjs create mode 100644 src/generators/json-simple/__tests__/index.test.mjs create mode 100644 src/generators/jsx-ast/__tests__/index.test.mjs create mode 100644 src/generators/jsx-ast/utils/__tests__/buildContent.test.mjs create mode 100644 src/generators/jsx-ast/utils/__tests__/buildSignature.test.mjs create mode 100644 src/generators/jsx-ast/utils/__tests__/getSortedHeadNodes.test.mjs create mode 100644 src/generators/jsx-ast/utils/__tests__/transformer.test.mjs delete mode 100644 src/generators/web/utils/__tests__/processing-more.test.mjs create mode 100644 src/logger/__tests__/index.smoke.test.mjs create mode 100644 src/logger/__tests__/index.test.mjs diff --git a/.c8rc.json b/.c8rc.json index 6f36280e..c4cc29c8 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -2,7 +2,6 @@ "all": true, "exclude": [ "eslint.config.mjs", - "**/legacy-html/**", "**/*.test.mjs", "**/fixtures", "src/generators/legacy-html/assets", diff --git a/bin/__tests__/cli.test.mjs b/bin/__tests__/cli.test.mjs new file mode 100644 index 00000000..c20e9d35 --- /dev/null +++ b/bin/__tests__/cli.test.mjs @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict'; +import { describe, it, mock } from 'node:test'; + +const logger = { + setLogLevel: mock.fn(), +}; + +describe('bin/cli', () => { + it('builds a program with commands/options and runs preAction hook', async () => { + const action = mock.fn(async () => {}); + + const commands = [ + { + name: 'mycmd', + description: 'My command', + options: { + requiredText: { + flags: ['--required-text '], + desc: 'Required option', + prompt: { type: 'text', required: true }, + }, + multi: { + flags: ['--multi '], + desc: 'Multi option', + prompt: { + type: 'multiselect', + options: [{ value: 'a' }, { value: 'b' }], + initialValue: ['a'], + }, + }, + }, + action, + }, + ]; + + const { createProgram } = await import('../cli.mjs'); + const program = createProgram(commands, { loggerInstance: logger }) + .exitOverride() + .configureOutput({ + writeOut: () => {}, + writeErr: () => {}, + }); + + // Global option should be present + const logLevelOpt = program.options.find( + o => o.attributeName() === 'logLevel' + ); + assert.ok(logLevelOpt); + + // Command and its options should be registered + const mycmd = program.commands.find(c => c.name() === 'mycmd'); + assert.ok(mycmd); + + const requiredOpt = mycmd.options.find( + o => o.attributeName() === 'requiredText' + ); + assert.ok(requiredOpt); + assert.equal(requiredOpt.mandatory, true); + + const multiOpt = mycmd.options.find(o => o.attributeName() === 'multi'); + assert.ok(multiOpt); + assert.deepEqual(multiOpt.argChoices, ['a', 'b']); + + await program.parseAsync([ + 'node', + 'cli', + '--log-level', + 'debug', + 'mycmd', + '--required-text', + 'hello', + '--multi', + 'a', + ]); + + assert.equal(logger.setLogLevel.mock.callCount(), 1); + assert.equal(action.mock.callCount(), 1); + }); +}); diff --git a/bin/__tests__/utils.test.mjs b/bin/__tests__/utils.test.mjs new file mode 100644 index 00000000..fc37d503 --- /dev/null +++ b/bin/__tests__/utils.test.mjs @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict'; +import process from 'node:process'; +import { describe, it, mock } from 'node:test'; + +const logger = { + error: mock.fn(), +}; + +mock.module('../../src/logger/index.mjs', { + defaultExport: logger, +}); + +const { errorWrap } = await import('../utils.mjs'); + +describe('bin/utils - errorWrap', () => { + it('returns wrapped result for sync functions', async () => { + const wrapped = errorWrap((a, b) => a + b); + const result = await wrapped(1, 2); + assert.equal(result, 3); + }); + + it('returns wrapped result for async functions', async () => { + const wrapped = errorWrap(async a => a * 2); + const result = await wrapped(4); + assert.equal(result, 8); + }); + + it('logs and exits when the wrapped function throws', async t => { + const exit = t.mock.method(process, 'exit'); + exit.mock.mockImplementation(() => {}); + + const err = new Error('boom'); + const wrapped = errorWrap(() => { + throw err; + }); + + await wrapped('x'); + + assert.equal(logger.error.mock.callCount(), 1); + assert.equal(logger.error.mock.calls[0].arguments[0], err); + assert.equal(exit.mock.callCount(), 1); + assert.equal(exit.mock.calls[0].arguments[0], 1); + }); +}); diff --git a/bin/cli.mjs b/bin/cli.mjs index 8100125f..1a843ec3 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node import process from 'node:process'; +import { pathToFileURL } from 'node:url'; import { Command, Option } from 'commander'; @@ -9,40 +10,64 @@ import { errorWrap } from './utils.mjs'; import { LogLevel } from '../src/logger/constants.mjs'; import logger from '../src/logger/index.mjs'; -const logLevelOption = new Option('--log-level ', 'Log level') - .choices(Object.keys(LogLevel)) - .default('info'); +/** + * + * @param commandsList + * @param root0 + * @param root0.loggerInstance + */ +export const createProgram = ( + commandsList = commands, + { loggerInstance = logger } = {} +) => { + const logLevelOption = new Option('--log-level ', 'Log level') + .choices(Object.keys(LogLevel)) + .default('info'); -const program = new Command() - .name('@nodejs/doc-kit') - .description('CLI tool to generate the Node.js API documentation') - .addOption(logLevelOption) - .hook('preAction', cmd => logger.setLogLevel(cmd.opts().logLevel)); + const program = new Command() + .name('@nodejs/doc-kit') + .description('CLI tool to generate the Node.js API documentation') + .addOption(logLevelOption) + .hook('preAction', cmd => loggerInstance.setLogLevel(cmd.opts().logLevel)); -// Registering commands -commands.forEach(({ name, description, options, action }) => { - const cmd = program.command(name).description(description); + // Registering commands + commandsList.forEach(({ name, description, options, action }) => { + const cmd = program.command(name).description(description); - // Add options to the command - Object.values(options).forEach(({ flags, desc, prompt }) => { - const option = new Option(flags.join(', '), desc).default( - prompt.initialValue - ); + // Add options to the command + Object.values(options).forEach(({ flags, desc, prompt }) => { + const option = new Option(flags.join(', '), desc).default( + prompt.initialValue + ); - if (prompt.required) { - option.makeOptionMandatory(); - } + if (prompt.required) { + option.makeOptionMandatory(); + } - if (prompt.type === 'multiselect') { - option.choices(prompt.options.map(({ value }) => value)); - } + if (prompt.type === 'multiselect') { + option.choices(prompt.options.map(({ value }) => value)); + } - cmd.addOption(option); + cmd.addOption(option); + }); + + // Set the action for the command + cmd.action(errorWrap(action)); }); - // Set the action for the command - cmd.action(errorWrap(action)); -}); + return program; +}; + +/** + * + * @param argv + */ +export const main = (argv = process.argv) => createProgram().parse(argv); -// Parse and execute command-line arguments -program.parse(process.argv); +// Parse and execute command-line arguments only when executed directly +if ( + process.argv[1] && + import.meta.url === pathToFileURL(process.argv[1]).href +) { + main(); +} diff --git a/bin/commands/__tests__/generate.test.mjs b/bin/commands/__tests__/generate.test.mjs new file mode 100644 index 00000000..3262efb0 --- /dev/null +++ b/bin/commands/__tests__/generate.test.mjs @@ -0,0 +1,87 @@ +import assert from 'node:assert/strict'; +import { resolve } from 'node:path'; +import { describe, it, mock } from 'node:test'; + +const runGenerators = mock.fn(async () => {}); + +mock.module('../../../src/generators.mjs', { + defaultExport: () => ({ runGenerators }), +}); + +mock.module('../../../src/parsers/markdown.mjs', { + namedExports: { + parseChangelog: async () => [{ version: 'v1.0.0', lts: false }], + parseIndex: async () => [{ section: 'fs', api: 'fs' }], + }, +}); + +mock.module('../../../src/parsers/json.mjs', { + namedExports: { + parseTypeMap: async () => ({ Foo: 'foo.html' }), + }, +}); + +const logger = { + debug: mock.fn(), +}; + +mock.module('../../../src/logger/index.mjs', { + defaultExport: logger, +}); + +mock.module('semver', { + namedExports: { + coerce: v => ({ raw: v, major: 1, minor: 2, patch: 3 }), + }, +}); + +// Ensure the prompt option label builder (map callback) runs during module load. +mock.module('../../../src/generators/index.mjs', { + namedExports: { + publicGenerators: { + web: { name: 'web', version: '1.2.3', description: 'Web output' }, + }, + }, +}); + +const cmd = (await import('../generate.mjs')).default; + +describe('bin/commands/generate', () => { + it('calls runGenerators with normalized options', async () => { + await cmd.action({ + target: ['web'], + input: ['doc/api/*.md'], + ignore: ['**/deprecated/**'], + output: 'out', + version: 'v20.0.0', + changelog: 'CHANGELOG.md', + gitRef: 'https://example.test/ref', + threads: '0', + chunkSize: 'not-a-number', + index: 'doc/api/index.md', + typeMap: 'doc/api/type_map.json', + }); + + assert.equal(logger.debug.mock.callCount(), 2); + assert.equal(runGenerators.mock.callCount(), 1); + + const args = runGenerators.mock.calls[0].arguments[0]; + + assert.deepEqual(args.generators, ['web']); + assert.deepEqual(args.input, ['doc/api/*.md']); + assert.deepEqual(args.ignore, ['**/deprecated/**']); + assert.equal(args.output, resolve('out')); + + // coerce() mocked: returns object with raw + assert.equal(args.version.raw, 'v20.0.0'); + + // min thread/chunkSize should be 1 when parseInt fails or < 1 + assert.equal(args.threads, 1); + assert.equal(args.chunkSize, 1); + + assert.equal(args.gitRef, 'https://example.test/ref'); + assert.deepEqual(args.releases, [{ version: 'v1.0.0', lts: false }]); + assert.deepEqual(args.index, [{ section: 'fs', api: 'fs' }]); + assert.deepEqual(args.typeMap, { Foo: 'foo.html' }); + }); +}); diff --git a/bin/commands/__tests__/interactive.branches.test.mjs b/bin/commands/__tests__/interactive.branches.test.mjs new file mode 100644 index 00000000..7ffd09f0 --- /dev/null +++ b/bin/commands/__tests__/interactive.branches.test.mjs @@ -0,0 +1,158 @@ +import assert from 'node:assert/strict'; +import process from 'node:process'; +import { describe, it, mock } from 'node:test'; + +const CANCEL = Symbol('cancel'); + +const prompts = { + intro: mock.fn(), + outro: mock.fn(), + cancel: mock.fn(), + isCancel: v => v === CANCEL, + select: mock.fn(async ({ message }) => { + // first select is the command selector + if (message === 'What would you like to do?') { + return 0; + } + // subsequent selects are option prompts + return 'a b'; + }), + multiselect: mock.fn(async () => ['safe', 'has space', "has'quote"]), + text: mock.fn(async ({ message, validate }) => { + // Ensure optional label is propagated + if (message.includes('(Optional)')) { + return 'value with space'; + } + + // Ensure required validation is wired + if (validate) { + assert.equal(validate(''), 'Value is required!'); + } + + return 'ok'; + }), + confirm: mock.fn(async ({ message }) => { + // Do not execute the generated command + if (message === 'Run now?') { + return false; + } + + // Option-level confirm should be false (boolean false branch) + return false; + }), +}; + +mock.module('@clack/prompts', { + namedExports: prompts, +}); + +const spawnSync = mock.fn(() => ({ status: 0 })); +mock.module('node:child_process', { + namedExports: { spawnSync }, +}); + +const logger = { + info: mock.fn(), +}; +mock.module('../../../src/logger/index.mjs', { + defaultExport: logger, +}); + +mock.module('../index.mjs', { + defaultExport: [ + { + name: 'custom', + description: 'Custom command', + options: { + optionalText: { + flags: ['--optional-text '], + desc: 'Optional text', + prompt: { type: 'text', message: 'Optional text?' }, + }, + requiredText: { + flags: ['--required-text '], + desc: 'Required text', + prompt: { type: 'text', message: 'Required text?', required: true }, + }, + confirmFlag: { + flags: ['--confirm-flag'], + desc: 'Confirm flag', + prompt: { type: 'confirm', message: 'Confirm?', initialValue: true }, + }, + pickOne: { + flags: ['--pick-one '], + desc: 'Pick one', + prompt: { + type: 'select', + message: 'Pick one?', + options: [{ label: 'A', value: 'a b' }], + }, + }, + multi: { + flags: ['--multi '], + desc: 'Pick many', + prompt: { + type: 'multiselect', + message: 'Pick many?', + options: [{ label: 'safe', value: 'safe' }], + }, + }, + }, + action: mock.fn(async () => {}), + }, + { + name: 'interactive', + description: 'Self', + options: {}, + action: async () => {}, + }, + ], +}); + +const cmd = (await import('../interactive.mjs')).default; + +describe('bin/commands/interactive (extra branches)', () => { + it('escapes args and handles optional/confirm/select/multiselect branches', async () => { + await cmd.action(); + + assert.equal(logger.info.mock.callCount(), 1); + + // Should not spawn when Run now? is false + assert.equal(spawnSync.mock.callCount(), 0); + + const logged = logger.info.mock.calls[0].arguments[0]; + + // Optional label should be applied + assert.equal(prompts.text.mock.callCount() >= 2, true); + + // select + multiselect should include escaped values (spaces + quotes) + assert.match(logged, /--pick-one\s+'a b'/); + assert.match(logged, /--multi\s+safe/); + assert.match(logged, /--multi\s+'has space'/); + assert.match(logged, /--multi\s+'has'\\''quote'/); + + // confirmFlag was false so it should not appear + assert.doesNotMatch(logged, /--confirm-flag/); + }); + + it('exits cleanly when cancelled during option prompts', async t => { + const originalExit = process.exit; + t.after(() => { + process.exit = originalExit; + }); + + process.exit = code => { + throw Object.assign(new Error('exit'), { code }); + }; + + const cancelText = mock.fn(async () => CANCEL); + prompts.text.mock.mockImplementation(cancelText); + + await assert.rejects(cmd.action(), err => { + assert.equal(err.code, 0); + return true; + }); + + assert.equal(prompts.cancel.mock.callCount() >= 1, true); + }); +}); diff --git a/bin/commands/__tests__/interactive.test.mjs b/bin/commands/__tests__/interactive.test.mjs new file mode 100644 index 00000000..a53255ec --- /dev/null +++ b/bin/commands/__tests__/interactive.test.mjs @@ -0,0 +1,141 @@ +import assert from 'node:assert/strict'; +import process from 'node:process'; +import { beforeEach, describe, it, mock } from 'node:test'; + +let selectCalls = 0; + +const prompts = { + intro: mock.fn(), + outro: mock.fn(), + cancel: mock.fn(), + isCancel: v => v === 'CANCEL', + select: mock.fn(async () => { + // 1st: command selection -> pick first command (generate) + // 2nd+ (if any): select prompts within options + selectCalls += 1; + return selectCalls === 1 ? 0 : 'value'; + }), + multiselect: mock.fn(async () => ['web']), + text: mock.fn(async ({ validate }) => { + // Only satisfy required fields. + if (validate) { + return "doc/api/foo'bar.md"; + } + return ''; // skip optionals + }), + confirm: mock.fn(async ({ message }) => { + // Don't execute spawned command by default + return message !== 'Run now?'; + }), +}; + +mock.module('@clack/prompts', { + namedExports: prompts, +}); + +const logger = { + info: mock.fn(), + debug: mock.fn(), + error: mock.fn(), + child: () => logger, +}; + +mock.module('../../../src/logger/index.mjs', { + defaultExport: logger, +}); + +const spawnSync = mock.fn(() => ({ status: 0 })); + +mock.module('node:child_process', { + namedExports: { spawnSync }, +}); + +const cmd = (await import('../interactive.mjs')).default; + +beforeEach(() => { + selectCalls = 0; + + prompts.intro.mock.resetCalls(); + prompts.outro.mock.resetCalls(); + prompts.cancel.mock.resetCalls(); + prompts.select.mock.resetCalls(); + prompts.multiselect.mock.resetCalls(); + prompts.text.mock.resetCalls(); + prompts.confirm.mock.resetCalls(); + + logger.info.mock.resetCalls(); + spawnSync.mock.resetCalls(); + + prompts.select.mock.mockImplementation(async () => { + selectCalls += 1; + return selectCalls === 1 ? 0 : 'value'; + }); + + prompts.text.mock.mockImplementation(async ({ validate }) => { + if (validate) { + return "doc/api/foo'bar.md"; + } + return ''; + }); + + prompts.multiselect.mock.mockImplementation(async () => ['web']); + + prompts.confirm.mock.mockImplementation(async ({ message }) => { + return message !== 'Run now?'; + }); +}); + +describe('bin/commands/interactive', () => { + it('builds a command and does not execute when Run now? is false', async () => { + await cmd.action(); + + assert.equal(logger.info.mock.callCount(), 1); + const msg = logger.info.mock.calls[0].arguments[0]; + assert.ok(msg.includes('Generated command')); + + // Ensure shell escaping happened for the single-quote. + assert.ok(msg.includes("'doc/api/foo'\\''bar.md'")); + + assert.equal(spawnSync.mock.callCount(), 0); + assert.equal(prompts.outro.mock.callCount(), 1); + }); + + it('executes the generated command when confirmed', async () => { + prompts.confirm.mock.mockImplementation(async ({ message }) => { + if (message === 'Run now?') { + return true; + } + return true; + }); + + await cmd.action(); + + assert.equal(spawnSync.mock.callCount(), 1); + const [execPath, args, opts] = spawnSync.mock.calls[0].arguments; + assert.equal(execPath, process.execPath); + assert.ok(Array.isArray(args)); + assert.equal(opts.stdio, 'inherit'); + }); + + it('exits with code 0 on cancellation at the first prompt', async t => { + const exit = t.mock.method(process, 'exit'); + exit.mock.mockImplementation(code => { + const err = new Error('process.exit'); + // attach code for debugging if needed + err.code = code; + throw err; + }); + + prompts.select.mock.mockImplementationOnce(async () => 'CANCEL'); + + try { + await cmd.action(); + } catch { + // expected: process.exit terminates execution in real CLI + } + + assert.equal(prompts.cancel.mock.callCount() >= 1, true); + assert.equal(exit.mock.callCount(), 1); + assert.equal(exit.mock.calls[0].arguments[0], 0); + }); +}); diff --git a/bin/commands/generate.mjs b/bin/commands/generate.mjs index 965ede11..d728a5ca 100644 --- a/bin/commands/generate.mjs +++ b/bin/commands/generate.mjs @@ -13,6 +13,16 @@ import { DEFAULT_TYPE_MAP } from '../../src/utils/parser/constants.mjs'; const availableGenerators = Object.keys(publicGenerators); +/** + * + * @param value + * @param min + */ +const parseMinInt = (value, min = 1) => { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) ? Math.max(parsed, min) : min; +}; + /** * @type {import('./types').Command} */ @@ -153,8 +163,8 @@ export default { version: coerce(opts.version), releases: await parseChangelog(opts.changelog), gitRef: opts.gitRef, - threads: Math.max(parseInt(opts.threads, 10), 1), - chunkSize: Math.max(parseInt(opts.chunkSize, 10), 1), + threads: parseMinInt(opts.threads, 1), + chunkSize: parseMinInt(opts.chunkSize, 1), index: await parseIndex(opts.index), typeMap: await parseTypeMap(opts.typeMap), }); diff --git a/src/generators/api-links/utils/__tests__/extractExports.test.mjs b/src/generators/api-links/utils/__tests__/extractExports.test.mjs new file mode 100644 index 00000000..9ee15ddc --- /dev/null +++ b/src/generators/api-links/utils/__tests__/extractExports.test.mjs @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { parse } from 'acorn'; + +import { extractExports } from '../extractExports.mjs'; + +describe('api-links/utils/extractExports', () => { + it('extracts assignments to exports.* and indirects', () => { + const program = parse( + [ + 'exports.foo = function () {}', + 'exports.bar = bar', + 'exports.baz = 123', + ].join('\n'), + { ecmaVersion: 'latest', locations: true, sourceType: 'script' } + ); + + const nameToLineNumberMap = {}; + const out = extractExports(program, 'fs', nameToLineNumberMap); + + assert.deepEqual(out.ctors, []); + assert.deepEqual(out.identifiers, ['baz']); + assert.deepEqual(out.indirects, { bar: 'fs.bar' }); + + assert.equal(nameToLineNumberMap['fs.foo'], 1); + }); + + it('extracts assignments to module.exports forms', () => { + const program = parse( + [ + 'module.exports = exports = { Foo, bar, baz: deprecate(qux, "x") }', + 'module.exports = new Thing()', + 'module.exports = something', + ].join('\n'), + { ecmaVersion: 'latest', locations: true, sourceType: 'script' } + ); + + const nameToLineNumberMap = {}; + const out = extractExports(program, 'buffer', nameToLineNumberMap); + + assert.deepEqual(out.ctors.sort(), ['Foo', 'Thing'].sort()); + assert.deepEqual( + out.identifiers.sort(), + ['Foo', 'bar', 'qux', 'something'].sort() + ); + }); + + it('extracts exports from variable declarations that assign to exports/module.exports', () => { + const program = parse( + [ + 'const x = exports.alpha = 1', + 'const Foo = module.exports = Foo', + 'const y = ignored = 123', + ].join('\n'), + { ecmaVersion: 'latest', locations: true, sourceType: 'script' } + ); + + const nameToLineNumberMap = {}; + const out = extractExports(program, 'tls', nameToLineNumberMap); + + assert.deepEqual(out.ctors, ['Foo']); + assert.deepEqual(out.identifiers, []); + + assert.equal(nameToLineNumberMap['tls.alpha'], 1); + assert.equal(nameToLineNumberMap['Foo'], 2); + }); +}); diff --git a/src/generators/ast/__tests__/index.test.mjs b/src/generators/ast/__tests__/index.test.mjs new file mode 100644 index 00000000..86d1d8aa --- /dev/null +++ b/src/generators/ast/__tests__/index.test.mjs @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import { join } from 'node:path'; +import { describe, it } from 'node:test'; + +import astGenerator from '../index.mjs'; + +describe('generators/ast', () => { + it('processChunk reads markdown files and returns parsed trees', async () => { + const tmp = await mkdtemp(join(os.tmpdir(), 'doc-kit-ast-')); + const f1 = join(tmp, 'a.md'); + const f2 = join(tmp, 'b.md'); + + await writeFile(f1, '## Hello\n\nStability: 1 - Experimental\n'); + await writeFile(f2, '# Title\n\nText\n'); + + const out = await astGenerator.processChunk([f1, f2], [0, 1]); + + assert.equal(out.length, 2); + assert.equal(out[0].file.basename, 'a.md'); + assert.equal(out[1].file.basename, 'b.md'); + assert.equal(out[0].tree.type, 'root'); + }); + + it('generate filters non-.md files and yields worker stream chunks', async () => { + const tmp = await mkdtemp(join(os.tmpdir(), 'doc-kit-ast-')); + const md = join(tmp, 'only.md'); + const txt = join(tmp, 'skip.txt'); + + await writeFile(md, '# A\n'); + await writeFile(txt, 'nope'); + + const seen = { files: null }; + + const worker = { + async *stream(files) { + seen.files = files; + yield [{ file: { basename: 'only.md' }, tree: { type: 'root' } }]; + }, + }; + + const gen = astGenerator.generate(undefined, { + input: [join(tmp, '*')], + worker, + }); + + const chunks = []; + for await (const c of gen) { + chunks.push(c); + } + + assert.deepEqual(seen.files, [md]); + assert.equal(chunks.length, 1); + }); +}); diff --git a/src/generators/json-simple/__tests__/index.test.mjs b/src/generators/json-simple/__tests__/index.test.mjs new file mode 100644 index 00000000..21f716bb --- /dev/null +++ b/src/generators/json-simple/__tests__/index.test.mjs @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import { describe, it, mock } from 'node:test'; + +const writeFile = mock.fn(async () => {}); + +mock.module('node:fs/promises', { + namedExports: { writeFile }, +}); + +const jsonSimple = (await import('../index.mjs')).default; + +describe('generators/json-simple', () => { + it('removes headings and stability nodes and does not mutate original', async () => { + const original = [ + { + api: 'fs', + content: { + type: 'root', + children: [ + { + type: 'heading', + depth: 1, + children: [{ type: 'text', value: 'T' }], + }, + { + type: 'blockquote', + children: [ + { + type: 'paragraph', + children: [ + { type: 'text', value: 'Stability: 1 - Experimental' }, + ], + }, + ], + }, + { type: 'paragraph', children: [{ type: 'text', value: 'Body' }] }, + ], + }, + }, + ]; + + const out = await jsonSimple.generate(original, {}); + + assert.equal(out.length, 1); + + // Original not mutated + assert.equal(original[0].content.children[0].type, 'heading'); + + // Mapped output has removed heading + stability + const types = out[0].content.children.map(n => n.type); + assert.deepEqual(types, ['paragraph']); + }); + + it('writes api-docs.json when output is provided', async () => { + writeFile.mock.resetCalls(); + + const input = [{ api: 'fs', content: { type: 'root', children: [] } }]; + + await jsonSimple.generate(input, { output: '/tmp/out' }); + + assert.equal(writeFile.mock.callCount(), 1); + const [path, content] = writeFile.mock.calls[0].arguments; + + assert.ok(String(path).endsWith('/api-docs.json')); + assert.equal(typeof content, 'string'); + }); +}); diff --git a/src/generators/jsx-ast/__tests__/index.test.mjs b/src/generators/jsx-ast/__tests__/index.test.mjs new file mode 100644 index 00000000..6ad81749 --- /dev/null +++ b/src/generators/jsx-ast/__tests__/index.test.mjs @@ -0,0 +1,125 @@ +import assert from 'node:assert/strict'; +import { describe, it, mock } from 'node:test'; + +const buildSideBarProps = mock.fn(() => ({ sidebar: true })); +mock.module('../utils/buildBarProps.mjs', { + namedExports: { buildSideBarProps }, +}); + +const buildContent = mock.fn(async (_entries, head, sideBarProps, remark) => ({ + type: 'content', + head, + sideBarProps, + remark, +})); +mock.module('../utils/buildContent.mjs', { + defaultExport: buildContent, +}); + +const getSortedHeadNodes = mock.fn(input => input.filter(n => n.isHead)); +mock.module('../utils/getSortedHeadNodes.mjs', { + namedExports: { getSortedHeadNodes }, +}); + +const groupNodesByModule = mock.fn(input => { + const map = new Map(); + for (const entry of input) { + const arr = map.get(entry.api) ?? []; + arr.push(entry); + map.set(entry.api, arr); + } + return map; +}); +mock.module('../../../utils/generators.mjs', { + namedExports: { groupNodesByModule }, +}); + +const remarkRecma = { id: 'remark' }; +mock.module('../../../utils/remark.mjs', { + namedExports: { getRemarkRecma: () => remarkRecma }, +}); + +const gen = (await import('../index.mjs')).default; + +describe('generators/jsx-ast/index', () => { + it('processChunk builds sidebar props and content per index', async () => { + const head1 = { api: 'fs', heading: { data: { name: 'fs' } } }; + const head2 = { api: 'tls', heading: { data: { name: 'tls' } } }; + + const slicedInput = [ + { head: head1, entries: [head1] }, + { head: head2, entries: [head2, { api: 'tls' }] }, + ]; + + const res = await gen.processChunk(slicedInput, [1], { + docPages: [], + releases: [], + version: { raw: 'v1.0.0' }, + }); + + assert.equal(buildSideBarProps.mock.callCount(), 1); + assert.equal(buildContent.mock.callCount(), 1); + + assert.equal(res.length, 1); + assert.equal(res[0].head.api, 'tls'); + assert.equal(res[0].remark, remarkRecma); + }); + + it('generate computes docPages and streams worker output', async () => { + const input = [ + { api: 'fs', isHead: true, heading: { data: { name: 'File system' } } }, + { api: 'tls', isHead: true, heading: { data: { name: 'TLS' } } }, + { api: 'fs', isHead: false, heading: { data: { name: 'Other' } } }, + ]; + + const streamArgs = []; + const worker = { + async *stream(entries, _allEntries, deps) { + streamArgs.push({ entries, deps }); + yield ['chunk']; + }, + }; + + const chunks = []; + for await (const chunk of gen.generate(input, { + index: undefined, + releases: [], + version: { raw: 'v1.0.0' }, + worker, + })) { + chunks.push(chunk); + } + + assert.deepEqual(chunks, [['chunk']]); + + assert.equal(streamArgs.length, 1); + assert.deepEqual(streamArgs[0].deps.docPages, [ + ['File system', 'fs.html'], + ['TLS', 'tls.html'], + ]); + }); + + it('generate uses provided index to compute docPages', async () => { + const input = [ + { api: 'fs', isHead: true, heading: { data: { name: 'File system' } } }, + ]; + + const worker = { + async *stream(_entries, _allEntries, deps) { + yield deps.docPages; + }, + }; + + const chunks = []; + for await (const chunk of gen.generate(input, { + index: [{ section: 'fs', api: 'fs' }], + releases: [], + version: { raw: 'v1.0.0' }, + worker, + })) { + chunks.push(chunk); + } + + assert.deepEqual(chunks, [[['fs', 'fs.html']]]); + }); +}); diff --git a/src/generators/jsx-ast/utils/__tests__/buildContent.test.mjs b/src/generators/jsx-ast/utils/__tests__/buildContent.test.mjs new file mode 100644 index 00000000..24a4f92f --- /dev/null +++ b/src/generators/jsx-ast/utils/__tests__/buildContent.test.mjs @@ -0,0 +1,276 @@ +import assert from 'node:assert/strict'; +import { describe, it, mock } from 'node:test'; + +mock.module('../../../../utils/queries/index.mjs', { + defaultExport: { + UNIST: { + isStabilityNode: node => node.type === 'blockquote' && node.data?.index, + isHeading: node => node.type === 'heading', + isTypedList: node => node.type === 'list' && node.data?.typed, + }, + }, +}); + +mock.module('mdast-util-slice-markdown', { + namedExports: { + slice: () => ({ + node: { + children: [ + { + children: [{ type: 'text', value: 'sliced' }], + }, + ], + }, + }), + }, +}); + +const createJSXElement = (name, props = {}) => ({ type: 'jsx', name, props }); + +mock.module('../ast.mjs', { + namedExports: { createJSXElement }, +}); + +const createPropertyTable = mock.fn(node => ({ type: 'table', from: node })); +mock.module('../buildPropertyTable.mjs', { + defaultExport: createPropertyTable, +}); + +const insertSignature = mock.fn(); +const getFullName = mock.fn(() => 'Full.Name'); +mock.module('../buildSignature.mjs', { + defaultExport: insertSignature, + namedExports: { getFullName }, +}); + +mock.module('../buildBarProps.mjs', { + namedExports: { + buildMetaBarProps: () => ({ meta: true }), + }, +}); + +const mod = await import('../buildContent.mjs'); + +const { + gatherChangeEntries, + createChangeElement, + createSourceLink, + extractHeadingContent, + createHeadingElement, + transformStabilityNode, + transformHeadingNode, + processEntry, + createDocumentLayout, +} = mod; + +const buildContent = mod.default; + +describe('jsx-ast/utils/buildContent', () => { + it('gathers lifecycle + explicit change entries', () => { + const remark = { + parse: mock.fn(() => ({})), + runSync: mock.fn(() => ({ body: [{ expression: 'parsed label' }] })), + }; + + const entry = { + added_in: 'v1.0.0', + changes: [ + { version: 'v2.0.0', description: '* hi', 'pr-url': 'https://x' }, + ], + }; + + const out = gatherChangeEntries(entry, remark); + + assert.equal(out.length, 2); + assert.equal(out[0].label, 'Added in: v1.0.0'); + assert.equal(out[1].label, 'parsed label'); + assert.equal(out[1].url, 'https://x'); + }); + + it('creates ChangeHistory element or returns null', () => { + const remark = { + parse: () => ({}), + runSync: () => ({ body: [{ expression: 'x' }] }), + }; + + assert.equal(createChangeElement({ changes: [] }, remark), null); + + const el = createChangeElement( + { changes: [{ version: 'v1.0.0', description: 'x' }] }, + remark + ); + assert.equal(el.type, 'jsx'); + assert.equal(el.name, 'ChangeHistory'); + assert.ok(Array.isArray(el.props.changes)); + }); + + it('creates source link element when provided', () => { + assert.equal(createSourceLink(undefined), null); + + const node = createSourceLink('lib/fs.js'); + assert.equal(node.tagName, 'span'); + const anchor = node.children[1]; + assert.equal(anchor.tagName, 'a'); + assert.match(anchor.properties.href, /lib\/fs\.js$/); + }); + + it('extractHeadingContent prefers inferred full name', () => { + const heading = { + type: 'heading', + children: ['fallback'], + data: { name: 'X', text: 'something', type: 'ctor' }, + }; + + assert.equal(extractHeadingContent(heading), 'Full.Name Constructor'); + }); + + it('creates heading element with icon + change element', () => { + const heading = { + type: 'heading', + children: [{ type: 'text', value: 'Hi' }], + data: { type: 'method', depth: 2, slug: 's', name: 'X', text: 'X' }, + }; + + const change = { type: 'jsx', name: 'ChangeHistory', props: {} }; + const out = createHeadingElement(heading, change); + + assert.equal(out.tagName, 'div'); + assert.equal(out.children[0].type, 'jsx'); + assert.equal(out.children[0].name, 'DataTag'); + assert.equal(out.children[out.children.length - 1], change); + }); + + it('transforms stability nodes into AlertBox', () => { + const node = { + type: 'blockquote', + data: { index: '1.0' }, + children: [{ type: 'paragraph', children: [] }], + }; + + const parent = { children: [node] }; + const res = transformStabilityNode(node, 0, parent); + + assert.equal(parent.children[0].type, 'jsx'); + assert.equal(parent.children[0].name, 'AlertBox'); + assert.equal(Array.isArray(res), true); + }); + + it('transforms deprecations headings with type box, source link, and signature insertion', () => { + const remark = { + parse: () => ({}), + runSync: () => ({ body: [{ expression: null }] }), + }; + + const heading = { + type: 'heading', + depth: 3, + children: [], + data: { + type: 'method', + slug: 'x', + name: 'X', + text: 'X', + }, + }; + + const typeNode = { type: 'paragraph', children: [] }; + const parent = { children: [heading, typeNode] }; + + const entry = { + api: 'deprecations', + source_link: 'lib/deprecations.js', + changes: [], + }; + + transformHeadingNode(entry, remark, heading, 0, parent); + + // Heading replaced with wrapper + assert.equal(parent.children[0].tagName, 'div'); + + // Source link inserted right after heading + assert.equal(parent.children[1].tagName, 'span'); + + // Type node replaced with AlertBox (shifted due to splice) + const alert = parent.children[2]; + assert.equal(alert.type, 'jsx'); + assert.equal(alert.name, 'AlertBox'); + assert.equal(alert.props.title, 'Type'); + + assert.equal(insertSignature.mock.callCount(), 1); + }); + + it('processEntry deep clones and transforms stability/heading/typed list nodes', () => { + const entry = { + api: 'fs', + source_link: undefined, + changes: [], + content: { + type: 'root', + children: [ + { + type: 'blockquote', + data: { index: '2' }, + children: [{ type: 'paragraph', children: [] }], + }, + { + type: 'heading', + children: [], + data: { type: 'misc', depth: 2, slug: 'h', name: 'H', text: 'H' }, + }, + { type: 'list', data: { typed: true }, children: [] }, + ], + }, + }; + + const remark = { + parse: () => ({}), + runSync: () => ({ body: [{ expression: null }] }), + }; + + const out = processEntry(entry, remark); + + assert.equal(out.children[0].type, 'jsx'); + assert.equal(out.children[1].tagName, 'div'); + assert.equal(out.children[2].type, 'table'); + + assert.equal(createPropertyTable.mock.callCount(), 1); + }); + + it('builds a document layout root', () => { + const root = createDocumentLayout( + [], + { sidebar: true }, + { meta: true }, + { + parse: () => ({}), + runSync: () => ({ body: [{ expression: null }] }), + } + ); + + assert.equal(root.type, 'root'); + assert.equal(root.children[0].type, 'jsx'); + assert.equal(root.children[0].name, 'NotificationProvider'); + }); + + it('buildContent returns the program expression with head data', async () => { + const remark = { + parse: () => ({}), + runSync: () => ({ body: [{ expression: null }] }), + run: async () => ({ + body: [{ expression: { type: 'Expression', value: 1 } }], + }), + }; + + const head = { + api: 'fs', + heading: { data: { name: 'fs' } }, + changes: [], + source_link: undefined, + content: { type: 'root', children: [] }, + }; + const res = await buildContent([head], head, { sidebar: true }, remark); + + assert.equal(res.type, 'Expression'); + assert.equal(res.data, head); + }); +}); diff --git a/src/generators/jsx-ast/utils/__tests__/buildPropertyTable.test.mjs b/src/generators/jsx-ast/utils/__tests__/buildPropertyTable.test.mjs index 95d42aff..9d89ba0d 100644 --- a/src/generators/jsx-ast/utils/__tests__/buildPropertyTable.test.mjs +++ b/src/generators/jsx-ast/utils/__tests__/buildPropertyTable.test.mjs @@ -1,67 +1,128 @@ import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; +import { describe, it, mock } from 'node:test'; -import createPropertyTable, { +// Avoid importing heavy query/remark/highlighter dependencies. +mock.module('../../../../utils/queries/index.mjs', { + defaultExport: { + QUERIES: { + typedListStarters: /^(Returns|Extends|Type):?\s*/, + }, + UNIST: { + isTypedList: node => node.type === 'list' && node.data?.typed, + }, + }, +}); + +const { + default: createPropertyTable, classifyTypeNode, extractPropertyName, extractTypeAnnotations, -} from '../buildPropertyTable.mjs'; + parseListIntoProperties, +} = await import('../buildPropertyTable.mjs'); describe('classifyTypeNode', () => { it('identifies union separator', () => { - const node = { type: 'text', value: ' | ' }; - assert.equal(classifyTypeNode(node), 2); + assert.equal(classifyTypeNode({ type: 'text', value: ' | ' }), 2); }); it('identifies type reference', () => { - const node = { - type: 'link', - children: [{ type: 'inlineCode', value: '' }], - }; - assert.equal(classifyTypeNode(node), 1); + assert.equal( + classifyTypeNode({ + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }), + 1 + ); }); it('returns 0 for non-type nodes', () => { - const node = { type: 'text', value: 'regular text' }; - assert.equal(classifyTypeNode(node), 0); + assert.equal(classifyTypeNode({ type: 'text', value: 'regular text' }), 0); + }); + + it('returns 0 for link nodes that are not type references', () => { + assert.equal( + classifyTypeNode({ + type: 'link', + children: [{ type: 'inlineCode', value: 'not-a-type' }], + }), + 0 + ); + assert.equal( + classifyTypeNode({ + type: 'link', + children: [{ type: 'text', value: '' }], + }), + 0 + ); }); }); describe('extractPropertyName', () => { + it('returns undefined for empty children', () => { + const children = []; + const result = extractPropertyName(children); + assert.equal(result, undefined); + }); + it('extracts name from inlineCode', () => { const children = [{ type: 'inlineCode', value: 'propName ' }]; const result = extractPropertyName(children); assert.equal(result.tagName, 'code'); assert.equal(result.children[0].value, 'propName'); }); + + it('extracts starters and trims matched prefix', () => { + const children = [{ type: 'text', value: 'Returns: something' }]; + const result = extractPropertyName(children); + assert.equal(result, 'Returns'); + assert.equal(children[0].value.trimStart().startsWith('something'), true); + }); + + it("returns false for 'Type' starter and removes empty text node", () => { + const children = [{ type: 'text', value: 'Type:' }]; + const result = extractPropertyName(children); + assert.equal(result, false); + assert.equal(children.length, 0); + }); + + it('returns undefined when no starter matches', () => { + const children = [{ type: 'text', value: 'NotAStarter: foo' }]; + const result = extractPropertyName(children); + assert.equal(result, undefined); + assert.equal(children.length, 1); + }); }); describe('extractTypeAnnotations', () => { - it('extracts type nodes until non-type node', () => { + it('extracts union type nodes until non-type node', () => { const children = [ - { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, + { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, { type: 'text', value: ' | ' }, - { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, + { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, { type: 'text', value: ' - description' }, ]; const result = extractTypeAnnotations(children); assert.equal(result.length, 3); - assert.equal(children.length, 1); // Only non-type node left + assert.equal(children.length, 1); assert.equal(children[0].value, ' - description'); }); - it('handles single type node', () => { + it('includes the node following a union separator (even if non-type)', () => { const children = [ - { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, - { type: 'text', value: ' description' }, + { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, + { type: 'text', value: ' | ' }, + { type: 'text', value: 'not a type' }, + { type: 'text', value: ' trailing' }, ]; const result = extractTypeAnnotations(children); - assert.equal(result.length, 1); + assert.equal(result.length, 3); assert.equal(children.length, 1); + assert.equal(children[0].value, ' trailing'); }); it('returns empty array for no type nodes', () => { @@ -72,13 +133,79 @@ describe('extractTypeAnnotations', () => { }); }); +describe('parseListIntoProperties', () => { + it('parses list items and retains nested typed sublists', () => { + const node = { + type: 'list', + children: [ + { + type: 'listItem', + children: [ + { + type: 'paragraph', + children: [ + { type: 'inlineCode', value: 'foo' }, + { type: 'text', value: ' ' }, + { + type: 'link', + children: [{ type: 'inlineCode', value: '' }], + }, + { type: 'text', value: ' description' }, + ], + }, + { + type: 'list', + data: { typed: true }, + children: [], + }, + ], + }, + ], + }; + + const props = parseListIntoProperties(node); + assert.equal(props.length, 1); + assert.ok(props[0].name); + assert.equal(props[0].types.length, 1); + assert.equal(props[0].sublist.type, 'list'); + assert.equal(props[0].desc[0].value.startsWith('description'), true); + }); + + it('handles Type-only items with empty description/types fallback', () => { + const node = { + type: 'list', + children: [ + { + type: 'listItem', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Type:' }], + }, + ], + }, + ], + }; + + const props = parseListIntoProperties(node); + assert.equal(props.length, 1); + assert.equal(props[0].name, false); + assert.equal(props[0].types.length, 0); + assert.equal(props[0].desc.length, 0); + assert.equal(props[0].sublist, undefined); + }); +}); + describe('createPropertyTable', () => { it('creates a table with headings by default', () => { const node = { + type: 'list', children: [ { + type: 'listItem', children: [ { + type: 'paragraph', children: [{ type: 'inlineCode', value: 'propName' }], }, ], @@ -87,7 +214,6 @@ describe('createPropertyTable', () => { }; const result = createPropertyTable(node); - assert.equal(result.tagName, 'table'); assert.ok(result.children.find(child => child.tagName === 'thead')); assert.ok(result.children.find(child => child.tagName === 'tbody')); @@ -95,10 +221,13 @@ describe('createPropertyTable', () => { it('creates a table without headings when specified', () => { const node = { + type: 'list', children: [ { + type: 'listItem', children: [ { + type: 'paragraph', children: [{ type: 'inlineCode', value: 'propName' }], }, ], @@ -110,5 +239,31 @@ describe('createPropertyTable', () => { assert.equal(result.tagName, 'table'); assert.ok(!result.children.find(child => child.tagName === 'thead')); + assert.equal(result.children[0].tagName, 'tr'); + }); + + it('renders dashes for empty cells (name/types/desc)', () => { + const node = { + type: 'list', + children: [ + { + type: 'listItem', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Type:' }], + }, + ], + }, + ], + }; + + const result = createPropertyTable(node); + const tbody = result.children.find(child => child.tagName === 'tbody'); + const row = tbody.children[0]; + assert.equal(row.tagName, 'tr'); + assert.equal(row.children[0].children[0].value, '-'); + assert.equal(row.children[1].children[0].value, '-'); + assert.equal(row.children[2].children[0].value, '-'); }); }); diff --git a/src/generators/jsx-ast/utils/__tests__/buildSignature.test.mjs b/src/generators/jsx-ast/utils/__tests__/buildSignature.test.mjs new file mode 100644 index 00000000..83c19fb0 --- /dev/null +++ b/src/generators/jsx-ast/utils/__tests__/buildSignature.test.mjs @@ -0,0 +1,137 @@ +import assert from 'node:assert/strict'; +import { describe, it, mock } from 'node:test'; + +const highlightToHast = mock.fn((code, lang) => ({ + type: 'element', + tagName: 'pre', + properties: { lang }, + children: [{ type: 'text', value: code }], +})); + +mock.module('../../../../utils/highlighter.mjs', { + namedExports: { + highlighter: { highlightToHast }, + }, +}); + +mock.module('../../../../utils/queries/index.mjs', { + defaultExport: { + UNIST: { + isTypedList: node => node.type === 'list', + }, + }, +}); + +mock.module('../../../legacy-json/utils/parseList.mjs', { + namedExports: { + parseListItem: item => item, + }, +}); + +const parseSignature = mock.fn((text, params) => { + if (text.includes('extends')) { + return { params, return: null, extends: { type: 'Base' } }; + } + + return { + params, + return: { type: 'number' }, + extends: null, + }; +}); + +mock.module('../../../legacy-json/utils/parseSignature.mjs', { + defaultExport: parseSignature, +}); + +const buildSignature = (await import('../buildSignature.mjs')).default; +const { generateSignature, getFullName } = + await import('../buildSignature.mjs'); + +describe('jsx-ast/utils/buildSignature', () => { + it('generateSignature formats class extends signature', () => { + const sig = generateSignature('Foo', { + params: [], + return: null, + extends: { type: 'Bar' }, + }); + assert.equal(sig, 'class Foo extends Bar'); + }); + + it('generateSignature formats params/return and marks optional params', () => { + const sig = generateSignature( + 'fn', + { + params: [ + { name: 'a', optional: true }, + { name: 'b', default: '1' }, + { name: 'c' }, + ], + return: { type: 'string' }, + extends: null, + }, + '' + ); + + assert.equal(sig, 'fn(a?, b?, c): string'); + }); + + it('getFullName prefers inline code containing the name', () => { + assert.equal( + getFullName({ name: 'bar', text: 'Use `foo.bar()` here' }), + 'foo.bar' + ); + assert.equal(getFullName({ name: 'foo', text: 'foo' }), 'foo'); + }); + + it('does not insert signature for class with no extends', () => { + const heading = { + type: 'heading', + data: { type: 'class', name: 'Foo', text: 'class Foo' }, + }; + const list = { type: 'list', children: [] }; + + const parent = { children: [heading, list] }; + + buildSignature(parent, heading, 0); + + assert.deepEqual(parent.children, [heading, list]); + }); + + it('inserts signature and removes extends list for classes with extends', () => { + const heading = { + type: 'heading', + data: { type: 'class', name: 'Foo', text: 'class Foo extends Base' }, + }; + const list = { type: 'list', children: [{ name: 'x' }] }; + + const parent = { children: [heading, list, { type: 'paragraph' }] }; + + buildSignature(parent, heading, 0); + + // First node should be inserted signature wrapper + assert.equal(parent.children[0].type, 'element'); + assert.equal(parent.children[0].tagName, 'div'); + + // Typed list removed + assert.equal( + parent.children.some(n => n === list), + false + ); + + assert.equal(highlightToHast.mock.callCount(), 1); + }); + + it('prefixes constructors with "new "', () => { + const heading = { + type: 'heading', + data: { type: 'ctor', name: 'Foo', text: 'new Foo()' }, + }; + const parent = { children: [heading] }; + + buildSignature(parent, heading, 0); + + const code = parent.children[0].children[0].children[0].value; + assert.match(code, /^new\s+/); + }); +}); diff --git a/src/generators/jsx-ast/utils/__tests__/getSortedHeadNodes.test.mjs b/src/generators/jsx-ast/utils/__tests__/getSortedHeadNodes.test.mjs new file mode 100644 index 00000000..1b51b05b --- /dev/null +++ b/src/generators/jsx-ast/utils/__tests__/getSortedHeadNodes.test.mjs @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { getSortedHeadNodes } from '../getSortedHeadNodes.mjs'; + +describe('getSortedHeadNodes', () => { + it('filters to depth 1 and sorts by overridden positions then name', () => { + const entries = [ + { api: 'zlib', heading: { depth: 1, data: { name: 'Zlib' } } }, + { api: 'index', heading: { depth: 1, data: { name: 'Index' } } }, + { api: 'fs', heading: { depth: 1, data: { name: 'File System' } } }, + { api: 'synopsis', heading: { depth: 1, data: { name: 'Synopsis' } } }, + { api: 'not-head', heading: { depth: 2, data: { name: 'Nope' } } }, + ]; + + const out = getSortedHeadNodes(entries); + + assert.deepEqual( + out.map(n => n.api), + ['index', 'synopsis', 'fs', 'zlib'] + ); + }); +}); diff --git a/src/generators/jsx-ast/utils/__tests__/transformer.test.mjs b/src/generators/jsx-ast/utils/__tests__/transformer.test.mjs new file mode 100644 index 00000000..59f84a50 --- /dev/null +++ b/src/generators/jsx-ast/utils/__tests__/transformer.test.mjs @@ -0,0 +1,128 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import createTransformer from '../transformer.mjs'; + +describe('jsx-ast/utils/transformer', () => { + it('wraps tables, applies data-labels, transforms tags, and moves footnotes', () => { + const tree = { + type: 'root', + children: [ + { type: 'element', tagName: 'pre', properties: {}, children: [] }, + { + type: 'element', + tagName: 'table', + properties: {}, + children: [ + { + type: 'element', + tagName: 'thead', + properties: {}, + children: [ + { + type: 'element', + tagName: 'tr', + properties: {}, + children: [ + { + type: 'element', + tagName: 'th', + properties: {}, + children: [{ type: 'text', value: 'A' }], + }, + { + type: 'element', + tagName: 'th', + properties: {}, + children: [{ type: 'text', value: 'B' }], + }, + ], + }, + ], + }, + { + type: 'element', + tagName: 'tbody', + properties: {}, + children: [ + { + type: 'element', + tagName: 'tr', + properties: {}, + children: [ + { + type: 'element', + tagName: 'td', + properties: {}, + children: [{ type: 'text', value: '1' }], + }, + { + type: 'element', + tagName: 'td', + properties: {}, + children: [{ type: 'text', value: '2' }], + }, + ], + }, + ], + }, + ], + }, + { + // document layout-ish placeholder: tree.children[2]?.children[1]?.children[0]?.children + type: 'element', + tagName: 'article', + properties: {}, + children: [ + { type: 'element', tagName: 'aside', properties: {}, children: [] }, + { + type: 'element', + tagName: 'div', + properties: {}, + children: [ + { + type: 'element', + tagName: 'main', + properties: {}, + children: [], + }, + ], + }, + ], + }, + { + type: 'element', + tagName: 'section', + properties: {}, + children: [ + { type: 'element', tagName: 'p', properties: {}, children: [] }, + ], + }, + ], + }; + + const transformer = createTransformer(); + transformer(tree); + + // pre -> CodeBox + assert.equal(tree.children[0].tagName, 'CodeBox'); + + // table wrapped in overflow container + assert.equal(tree.children[1].tagName, 'div'); + assert.equal( + tree.children[1].properties.className[0], + 'overflow-container' + ); + + const table = tree.children[1].children[0]; + const tbody = table.children.find(n => n.tagName === 'tbody'); + const firstRow = tbody.children[0]; + + assert.equal(firstRow.children[0].properties['data-label'], 'A'); + assert.equal(firstRow.children[1].properties['data-label'], 'B'); + + // footnotes section moved into main + const main = tree.children[2].children[1].children[0]; + assert.equal(main.children.length, 1); + }); +}); diff --git a/src/generators/web/utils/__tests__/processing-more.test.mjs b/src/generators/web/utils/__tests__/processing-more.test.mjs deleted file mode 100644 index 0eb0218b..00000000 --- a/src/generators/web/utils/__tests__/processing-more.test.mjs +++ /dev/null @@ -1,120 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import { - convertJSXToCode, - executeServerCode, - processJSXEntries, -} from '../processing.mjs'; - -describe('generators/web/utils - processing (more)', () => { - it('convertJSXToCode uses injected toJs and builders', () => { - const entries = [{ data: { api: 'myapi' } }]; - - const toJsMock = () => ({ value: 'RAW_CODE' }); - - const buildServerProgram = code => `SERVER:${code}`; - const buildClientProgram = code => `CLIENT:${code}`; - - const { serverCodeMap, clientCodeMap } = convertJSXToCode( - entries, - { buildServerProgram, buildClientProgram }, - { toJs: toJsMock } - ); - - assert.equal(serverCodeMap.get('myapi.jsx'), 'SERVER:RAW_CODE'); - assert.equal(clientCodeMap.get('myapi.jsx'), 'CLIENT:RAW_CODE'); - }); - - it('executeServerCode resolves otherChunks via chunked require', async () => { - const serverCodeMap = new Map([[`api.jsx`, 'ignored']]); - - const fakeRequire = () => ({ ok: true }); - - const mockBundle = async () => ({ - chunks: [ - { - fileName: 'lib.1.js', - isEntry: false, - code: 'module.exports = "LIB";', - }, - { - fileName: 'api.js', - isEntry: true, - code: 'const lib = require("./lib.1.js"); return lib + "-OK";', - }, - ], - css: 'body{}', - }); - - const { pages, css } = await executeServerCode(serverCodeMap, fakeRequire, { - bundleCode: mockBundle, - createChunkedRequire: (chunks, req) => id => { - if (id === './lib.1.js') { - return 'LIB'; - } - return req(id); - }, - }); - - assert.equal(pages.get('api.js'), 'LIB-OK'); - assert.equal(css, 'body{}'); - }); - - it('processJSXEntries end-to-end with overrides', async () => { - const entries = [ - { data: { api: 'api', heading: { data: { name: 'My API' } } } }, - ]; - - const template = - '{{title}}{{dehydrated}}{{importMap}}{{entrypoint}}{{speculationRules}}'; - - const astBuilders = { - buildServerProgram: code => `SERVER:${code}`, - buildClientProgram: code => `CLIENT:${code}`, - }; - - const fakeRequire = () => ({ fs: true }); - - const serverBundle = { - chunks: [ - { - fileName: 'api.js', - isEntry: true, - code: 'return "
server
";', - }, - ], - css: 's{}', - }; - - const clientBundle = { - chunks: [{ fileName: 'api.js', isEntry: true, code: '/* client */' }], - css: 'c{}', - importMap: '{}', - }; - - const bundleCode = async (map, opts) => - opts && opts.server ? serverBundle : clientBundle; - - const overrides = { - bundleCode, - createChunkedRequire: (chunks, req) => req, - transform: ({ code }) => ({ code: Buffer.from(String(code)) }), - toJs: () => ({ value: '/* generated */' }), - }; - - const { results, css } = await processJSXEntries( - entries, - template, - astBuilders, - fakeRequire, - { version: { version: '1.0.0' }, overrides } - ); - - assert.equal(results.length, 1); - const html = results[0].html.toString(); - assert.match(html, /
server<\/div>/); - assert.match(html, /My API/); - assert.ok(String(css).includes('s{}') || String(css).includes('c{}')); - }); -}); diff --git a/src/generators/web/utils/__tests__/processing.test.mjs b/src/generators/web/utils/__tests__/processing.test.mjs index e2d60a69..9b07b3a5 100644 --- a/src/generators/web/utils/__tests__/processing.test.mjs +++ b/src/generators/web/utils/__tests__/processing.test.mjs @@ -1,7 +1,120 @@ -import { describe } from 'node:test'; +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; -describe.skip('generators/web/utils - processing (skipped)', () => { - // Tests for `processing.mjs` are omitted because they relied on - // experimental module mocking. They should be re-added using - // a stable approach (e.g., dependency injection or test doubles). +import { + convertJSXToCode, + executeServerCode, + processJSXEntries, +} from '../processing.mjs'; + +describe('generators/web/utils - processing', () => { + it('convertJSXToCode uses injected toJs and builders', () => { + const entries = [{ data: { api: 'myapi' } }]; + + const toJsMock = () => ({ value: 'RAW_CODE' }); + + const buildServerProgram = code => `SERVER:${code}`; + const buildClientProgram = code => `CLIENT:${code}`; + + const { serverCodeMap, clientCodeMap } = convertJSXToCode( + entries, + { buildServerProgram, buildClientProgram }, + { toJs: toJsMock } + ); + + assert.equal(serverCodeMap.get('myapi.jsx'), 'SERVER:RAW_CODE'); + assert.equal(clientCodeMap.get('myapi.jsx'), 'CLIENT:RAW_CODE'); + }); + + it('executeServerCode resolves otherChunks via chunked require', async () => { + const serverCodeMap = new Map([[`api.jsx`, 'ignored']]); + + const fakeRequire = () => ({ ok: true }); + + const mockBundle = async () => ({ + chunks: [ + { + fileName: 'lib.1.js', + isEntry: false, + code: 'module.exports = "LIB";', + }, + { + fileName: 'api.js', + isEntry: true, + code: 'const lib = require("./lib.1.js"); return lib + "-OK";', + }, + ], + css: 'body{}', + }); + + const { pages, css } = await executeServerCode(serverCodeMap, fakeRequire, { + bundleCode: mockBundle, + createChunkedRequire: (chunks, req) => id => { + if (id === './lib.1.js') { + return 'LIB'; + } + return req(id); + }, + }); + + assert.equal(pages.get('api.js'), 'LIB-OK'); + assert.equal(css, 'body{}'); + }); + + it('processJSXEntries end-to-end with overrides', async () => { + const entries = [ + { data: { api: 'api', heading: { data: { name: 'My API' } } } }, + ]; + + const template = + '{{title}}{{dehydrated}}{{importMap}}{{entrypoint}}{{speculationRules}}'; + + const astBuilders = { + buildServerProgram: code => `SERVER:${code}`, + buildClientProgram: code => `CLIENT:${code}`, + }; + + const fakeRequire = () => ({ fs: true }); + + const serverBundle = { + chunks: [ + { + fileName: 'api.js', + isEntry: true, + code: 'return "
server
";', + }, + ], + css: 's{}', + }; + + const clientBundle = { + chunks: [{ fileName: 'api.js', isEntry: true, code: '/* client */' }], + css: 'c{}', + importMap: '{}', + }; + + const bundleCode = async (map, opts) => + opts && opts.server ? serverBundle : clientBundle; + + const overrides = { + bundleCode, + createChunkedRequire: (chunks, req) => req, + transform: ({ code }) => ({ code: Buffer.from(String(code)) }), + toJs: () => ({ value: '/* generated */' }), + }; + + const { results, css } = await processJSXEntries( + entries, + template, + astBuilders, + fakeRequire, + { version: { version: '1.0.0' }, overrides } + ); + + assert.equal(results.length, 1); + const html = results[0].html.toString(); + assert.match(html, /
server<\/div>/); + assert.match(html, /My API/); + assert.ok(String(css).includes('s{}') || String(css).includes('c{}')); + }); }); diff --git a/src/logger/__tests__/index.smoke.test.mjs b/src/logger/__tests__/index.smoke.test.mjs new file mode 100644 index 00000000..853041c8 --- /dev/null +++ b/src/logger/__tests__/index.smoke.test.mjs @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import logger, { Logger } from '../index.mjs'; + +describe('logger index (smoke)', () => { + it('exports a default logger instance', () => { + assert.equal(typeof logger.info, 'function'); + assert.equal(typeof logger.error, 'function'); + assert.equal(typeof logger.setLogLevel, 'function'); + }); + + it('can create a console logger via Logger()', () => { + const consoleLogger = Logger('console'); + assert.equal(typeof consoleLogger.info, 'function'); + assert.equal(typeof consoleLogger.setLogLevel, 'function'); + }); +}); diff --git a/src/logger/__tests__/index.test.mjs b/src/logger/__tests__/index.test.mjs new file mode 100644 index 00000000..c0dd43b1 --- /dev/null +++ b/src/logger/__tests__/index.test.mjs @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import { describe, it, mock } from 'node:test'; + +const createLogger = mock.fn(transport => ({ transport })); + +mock.module('../logger.mjs', { + namedExports: { createLogger }, +}); + +const transports = { + console: { name: 'console-transport' }, + file: { name: 'file-transport' }, +}; + +mock.module('../transports/index.mjs', { + namedExports: { transports }, +}); + +describe('logger/index', () => { + it('creates a logger with requested transport', async () => { + const mod = await import('../index.mjs'); + + const instance = mod.Logger('file'); + + assert.equal(createLogger.mock.callCount(), 2); + assert.deepEqual(instance, { transport: transports.file }); + + // Default export should use console transport + assert.deepEqual(mod.default, { transport: transports.console }); + }); + + it('throws on unknown transport', async () => { + const { Logger } = await import('../index.mjs'); + assert.throws(() => Logger('missing'), /Transport 'missing' not found/); + }); +}); diff --git a/src/utils/__tests__/highlighter.test.mjs b/src/utils/__tests__/highlighter.test.mjs index 9c00ce40..867316e5 100644 --- a/src/utils/__tests__/highlighter.test.mjs +++ b/src/utils/__tests__/highlighter.test.mjs @@ -1,49 +1,100 @@ import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; +import { describe, it, mock } from 'node:test'; -import rehypeShikiji from '../highlighter.mjs'; +const codeToHast = mock.fn((code, { lang }) => ({ + children: [ + { + type: 'element', + tagName: 'pre', + properties: { class: 'shiki', style: `lang:${lang}` }, + children: [ + { + type: 'element', + tagName: 'code', + properties: { class: 'code' }, + children: [{ type: 'text', value: code }], + }, + ], + }, + ], +})); -describe('utils - highlighter', () => { - it('handles pre elements with missing code child gracefully', () => { +mock.module('@node-core/rehype-shiki', { + defaultExport: async () => ({ shiki: { codeToHast } }), +}); + +const rehypeShikiji = (await import('../highlighter.mjs')).default; + +describe('utils/highlighter rehypeShikiji', () => { + it('highlights
', () => {
     const tree = {
       type: 'root',
       children: [
         {
           type: 'element',
           tagName: 'pre',
-          children: [{ type: 'text', value: 'no code' }],
+          properties: {},
+          children: [
+            {
+              type: 'element',
+              tagName: 'code',
+              properties: { className: ['language-js'] },
+              children: [{ type: 'text', value: 'const a = 1' }],
+            },
+          ],
+        },
+        // ignored: missing className
+        {
+          type: 'element',
+          tagName: 'pre',
+          properties: {},
+          children: [
+            {
+              type: 'element',
+              tagName: 'code',
+              properties: {},
+              children: [{ type: 'text', value: 'noop' }],
+            },
+          ],
         },
       ],
     };
 
-    const plugin = rehypeShikiji();
-    // should not throw
-    plugin(tree);
+    rehypeShikiji()(tree);
+
+    assert.equal(codeToHast.mock.callCount(), 1);
+
+    const highlighted = tree.children[0];
+    assert.equal(highlighted.tagName, 'pre');
+    assert.match(highlighted.properties.class, /language-js/);
+
+    // Copy button added
+    const copyButton = highlighted.children[highlighted.children.length - 1];
+    assert.equal(copyButton.tagName, 'button');
   });
 
   it('creates switchable code tab for two code blocks', () => {
-    const code1 = { type: 'element', tagName: 'code', properties: {} };
-    const pre1 = {
+    const makePre = language => ({
       type: 'element',
       tagName: 'pre',
-      children: [code1],
-      properties: { class: 'language-cjs', style: 's1' },
-    };
+      properties: { class: `language-${language}`, style: 's' },
+      children: [
+        {
+          type: 'element',
+          tagName: 'code',
+          properties: {},
+          children: [{ type: 'text', value: `// ${language}` }],
+        },
+      ],
+    });
 
-    const code2 = { type: 'element', tagName: 'code', properties: {} };
-    const pre2 = {
-      type: 'element',
-      tagName: 'pre',
-      children: [code2],
-      properties: { class: 'language-mjs', style: 's2' },
+    const tree = {
+      type: 'root',
+      children: [makePre('cjs'), makePre('mjs')],
     };
 
-    const tree = { type: 'root', children: [pre1, pre2] };
-
-    const plugin = rehypeShikiji();
-    plugin(tree);
+    rehypeShikiji()(tree);
 
-    // first child should be replaced with a pre element (switchable container)
     const first = tree.children[0];
     assert.equal(first.tagName, 'pre');
     const hasShikiClass =
@@ -54,5 +105,7 @@ describe('utils - highlighter', () => {
         Array.isArray(first.properties.className) &&
         first.properties.className.includes('shiki'));
     assert.ok(hasShikiClass);
+    assert.equal(first.children.filter(n => n.tagName === 'code').length, 2);
+    assert.equal(first.children[first.children.length - 1].tagName, 'button');
   });
 });

From f153e98e44508c1c210d6b8daa78213f459cdb5f Mon Sep 17 00:00:00 2001
From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com>
Date: Mon, 15 Dec 2025 11:24:50 +0100
Subject: [PATCH 5/6] WIP

---
 ...test.mjs => interactive-branches.test.mjs} |  0
 src/__tests__/generators.test.mjs             |  2 +-
 src/__tests__/metadata.test.mjs               |  2 +-
 src/__tests__/streaming.test.mjs              |  2 +-
 .../utils/__tests__/safeCopy.test.mjs         |  4 +--
 .../utils/__tests__/buildSection.test.mjs     | 16 +++++------
 .../utils/__tests__/converter.test.mjs        |  2 +-
 .../metadata/__tests__/index.test.mjs         |  2 +-
 .../orama-db/__tests__/generate.test.mjs      |  2 +-
 src/logger/__tests__/index.smoke.test.mjs     | 18 ------------
 src/logger/__tests__/logger.test.mjs          | 17 ++++++++++-
 src/parsers/__tests__/markdown.test.mjs       |  2 --
 src/threading/__tests__/parallel.test.mjs     |  2 +-
 src/utils/__tests__/parser.test.mjs           | 28 +++++++++++++------
 src/utils/__tests__/unist.test.mjs            |  2 +-
 src/utils/parser/__tests__/index.test.mjs     |  2 +-
 src/utils/queries/__tests__/index.test.mjs    |  2 +-
 17 files changed, 54 insertions(+), 51 deletions(-)
 rename bin/commands/__tests__/{interactive.branches.test.mjs => interactive-branches.test.mjs} (100%)
 delete mode 100644 src/logger/__tests__/index.smoke.test.mjs

diff --git a/bin/commands/__tests__/interactive.branches.test.mjs b/bin/commands/__tests__/interactive-branches.test.mjs
similarity index 100%
rename from bin/commands/__tests__/interactive.branches.test.mjs
rename to bin/commands/__tests__/interactive-branches.test.mjs
diff --git a/src/__tests__/generators.test.mjs b/src/__tests__/generators.test.mjs
index 7464784e..56af8eff 100644
--- a/src/__tests__/generators.test.mjs
+++ b/src/__tests__/generators.test.mjs
@@ -1,4 +1,4 @@
-import { ok, strictEqual } from 'node:assert';
+import { ok, strictEqual } from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import createGenerator from '../generators.mjs';
diff --git a/src/__tests__/metadata.test.mjs b/src/__tests__/metadata.test.mjs
index 8cafb4ab..ec468ea3 100644
--- a/src/__tests__/metadata.test.mjs
+++ b/src/__tests__/metadata.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual, deepStrictEqual } from 'node:assert';
+import { strictEqual, deepStrictEqual } from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import GitHubSlugger from 'github-slugger';
diff --git a/src/__tests__/streaming.test.mjs b/src/__tests__/streaming.test.mjs
index 6b952f54..23aa7a9d 100644
--- a/src/__tests__/streaming.test.mjs
+++ b/src/__tests__/streaming.test.mjs
@@ -1,4 +1,4 @@
-import { deepStrictEqual, ok, strictEqual } from 'node:assert';
+import { deepStrictEqual, ok, strictEqual } from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import {
diff --git a/src/generators/legacy-html/utils/__tests__/safeCopy.test.mjs b/src/generators/legacy-html/utils/__tests__/safeCopy.test.mjs
index 186020a8..06c427a0 100644
--- a/src/generators/legacy-html/utils/__tests__/safeCopy.test.mjs
+++ b/src/generators/legacy-html/utils/__tests__/safeCopy.test.mjs
@@ -1,6 +1,4 @@
-'use strict';
-
-import assert from 'node:assert';
+import assert from 'node:assert/strict';
 import { mkdir, readFile, rm, utimes, writeFile } from 'node:fs/promises';
 import { join } from 'node:path';
 import { afterEach, beforeEach, describe, it } from 'node:test';
diff --git a/src/generators/legacy-json/utils/__tests__/buildSection.test.mjs b/src/generators/legacy-json/utils/__tests__/buildSection.test.mjs
index 71eeadc3..cea01d95 100644
--- a/src/generators/legacy-json/utils/__tests__/buildSection.test.mjs
+++ b/src/generators/legacy-json/utils/__tests__/buildSection.test.mjs
@@ -1,7 +1,5 @@
-'use strict';
-
 import assert from 'node:assert/strict';
-import { describe, test } from 'node:test';
+import { describe, it } from 'node:test';
 
 import { UNPROMOTED_KEYS } from '../../constants.mjs';
 import { promoteMiscChildren } from '../buildSection.mjs';
@@ -22,7 +20,7 @@ describe('promoteMiscChildren', () => {
     });
   }
 
-  test('ignores non-misc section', () => {
+  it('ignores non-misc section', () => {
     const section = buildReadOnlySection({
       type: 'text',
     });
@@ -37,7 +35,7 @@ describe('promoteMiscChildren', () => {
     promoteMiscChildren(section, parent);
   });
 
-  test('ignores misc parent', () => {
+  it('ignores misc parent', () => {
     const section = buildReadOnlySection({
       type: 'misc',
     });
@@ -52,7 +50,7 @@ describe('promoteMiscChildren', () => {
     promoteMiscChildren(section, parent);
   });
 
-  test('ignores keys in UNPROMOTED_KEYS', () => {
+  it('ignores keys in UNPROMOTED_KEYS', () => {
     const sectionRaw = {
       type: 'misc',
       promotableKey: 'this should be promoted',
@@ -88,7 +86,7 @@ describe('promoteMiscChildren', () => {
   });
 
   describe('merges properties correctly', () => {
-    test('pushes child property if parent is an array', () => {
+    it('pushes child property if parent is an array', () => {
       const section = buildReadOnlySection({
         type: 'misc',
         someValue: 'bar',
@@ -104,7 +102,7 @@ describe('promoteMiscChildren', () => {
       assert.deepStrictEqual(parent.someValue, ['foo', 'bar']);
     });
 
-    test('ignores child property if parent has a value that is not an array', () => {
+    it('ignores child property if parent has a value that is not an array', () => {
       const section = buildReadOnlySection({
         type: 'misc',
         someValue: 'bar',
@@ -120,7 +118,7 @@ describe('promoteMiscChildren', () => {
       assert.strictEqual(parent.someValue, 'foo');
     });
 
-    test('promotes child property if parent does not have the property', () => {
+    it('promotes child property if parent does not have the property', () => {
       const section = buildReadOnlySection({
         type: 'misc',
         someValue: 'bar',
diff --git a/src/generators/man-page/utils/__tests__/converter.test.mjs b/src/generators/man-page/utils/__tests__/converter.test.mjs
index 505a9b70..bec35edd 100644
--- a/src/generators/man-page/utils/__tests__/converter.test.mjs
+++ b/src/generators/man-page/utils/__tests__/converter.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual } from 'node:assert';
+import { strictEqual } from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import { u } from 'unist-builder';
diff --git a/src/generators/metadata/__tests__/index.test.mjs b/src/generators/metadata/__tests__/index.test.mjs
index ab4494a4..41ef27af 100644
--- a/src/generators/metadata/__tests__/index.test.mjs
+++ b/src/generators/metadata/__tests__/index.test.mjs
@@ -1,4 +1,4 @@
-import { deepStrictEqual, strictEqual } from 'node:assert';
+import { deepStrictEqual, strictEqual } from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import generator from '../index.mjs';
diff --git a/src/generators/orama-db/__tests__/generate.test.mjs b/src/generators/orama-db/__tests__/generate.test.mjs
index d3b16470..6273db4c 100644
--- a/src/generators/orama-db/__tests__/generate.test.mjs
+++ b/src/generators/orama-db/__tests__/generate.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual, rejects, ok } from 'node:assert';
+import { strictEqual, rejects, ok } from 'node:assert/strict';
 import { mkdtemp, readFile, rm } from 'node:fs/promises';
 import { tmpdir } from 'node:os';
 import { join } from 'node:path';
diff --git a/src/logger/__tests__/index.smoke.test.mjs b/src/logger/__tests__/index.smoke.test.mjs
deleted file mode 100644
index 853041c8..00000000
--- a/src/logger/__tests__/index.smoke.test.mjs
+++ /dev/null
@@ -1,18 +0,0 @@
-import assert from 'node:assert/strict';
-import { describe, it } from 'node:test';
-
-import logger, { Logger } from '../index.mjs';
-
-describe('logger index (smoke)', () => {
-  it('exports a default logger instance', () => {
-    assert.equal(typeof logger.info, 'function');
-    assert.equal(typeof logger.error, 'function');
-    assert.equal(typeof logger.setLogLevel, 'function');
-  });
-
-  it('can create a console logger via Logger()', () => {
-    const consoleLogger = Logger('console');
-    assert.equal(typeof consoleLogger.info, 'function');
-    assert.equal(typeof consoleLogger.setLogLevel, 'function');
-  });
-});
diff --git a/src/logger/__tests__/logger.test.mjs b/src/logger/__tests__/logger.test.mjs
index 1673b453..da12fb51 100644
--- a/src/logger/__tests__/logger.test.mjs
+++ b/src/logger/__tests__/logger.test.mjs
@@ -1,7 +1,8 @@
-import { deepStrictEqual, strictEqual } from 'node:assert';
+import { deepStrictEqual, strictEqual } from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import { LogLevel } from '../constants.mjs';
+import logger, { Logger } from '../index.mjs';
 import { createLogger } from '../logger.mjs';
 
 /**
@@ -381,3 +382,17 @@ describe('createLogger', () => {
     });
   });
 });
+
+describe('logger (smoke)', () => {
+  it('exports a default logger instance', () => {
+    strictEqual(typeof logger.info, 'function');
+    strictEqual(typeof logger.error, 'function');
+    strictEqual(typeof logger.setLogLevel, 'function');
+  });
+
+  it('can create a console logger via Logger()', () => {
+    const consoleLogger = Logger('console');
+    strictEqual(typeof consoleLogger.info, 'function');
+    strictEqual(typeof consoleLogger.setLogLevel, 'function');
+  });
+});
diff --git a/src/parsers/__tests__/markdown.test.mjs b/src/parsers/__tests__/markdown.test.mjs
index 14bd071f..3119c255 100644
--- a/src/parsers/__tests__/markdown.test.mjs
+++ b/src/parsers/__tests__/markdown.test.mjs
@@ -1,5 +1,3 @@
-'use strict';
-
 import assert from 'node:assert/strict';
 import { describe, it, mock } from 'node:test';
 
diff --git a/src/threading/__tests__/parallel.test.mjs b/src/threading/__tests__/parallel.test.mjs
index 41b04c6f..53622617 100644
--- a/src/threading/__tests__/parallel.test.mjs
+++ b/src/threading/__tests__/parallel.test.mjs
@@ -1,4 +1,4 @@
-import { deepStrictEqual, ok, strictEqual } from 'node:assert';
+import { deepStrictEqual, ok, strictEqual } from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import createWorkerPool from '../index.mjs';
diff --git a/src/utils/__tests__/parser.test.mjs b/src/utils/__tests__/parser.test.mjs
index ff906a1f..6c807f92 100644
--- a/src/utils/__tests__/parser.test.mjs
+++ b/src/utils/__tests__/parser.test.mjs
@@ -1,7 +1,5 @@
-'use strict';
-
 import assert from 'node:assert/strict';
-import { describe, it, mock } from 'node:test';
+import { afterEach, beforeEach, describe, it, mock } from 'node:test';
 
 mock.module('node:fs/promises', {
   namedExports: {
@@ -9,11 +7,20 @@ mock.module('node:fs/promises', {
   },
 });
 
-global.fetch = mock.fn(() =>
-  Promise.resolve({
-    text: () => Promise.resolve('fetched content'),
-  })
-);
+let originalFetch;
+
+beforeEach(() => {
+  originalFetch = globalThis.fetch;
+  globalThis.fetch = mock.fn(() =>
+    Promise.resolve({
+      text: () => Promise.resolve('fetched content'),
+    })
+  );
+});
+
+afterEach(() => {
+  globalThis.fetch = originalFetch;
+});
 
 const { loadFromURL } = await import('../parser.mjs');
 
@@ -27,4 +34,9 @@ describe('loadFromURL', () => {
     const result = await loadFromURL('https://example.com/data');
     assert.equal(result, 'fetched content');
   });
+
+  it('should load content from a URL object', async () => {
+    const result = await loadFromURL(new URL('https://example.com/data'));
+    assert.equal(result, 'fetched content');
+  });
 });
diff --git a/src/utils/__tests__/unist.test.mjs b/src/utils/__tests__/unist.test.mjs
index a67de08c..ead77fe8 100644
--- a/src/utils/__tests__/unist.test.mjs
+++ b/src/utils/__tests__/unist.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual } from 'node:assert';
+import { strictEqual } from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import { transformNodesToString, callIfBefore } from '../unist.mjs';
diff --git a/src/utils/parser/__tests__/index.test.mjs b/src/utils/parser/__tests__/index.test.mjs
index ae411291..3c8f59f3 100644
--- a/src/utils/parser/__tests__/index.test.mjs
+++ b/src/utils/parser/__tests__/index.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual, deepStrictEqual } from 'node:assert';
+import { strictEqual, deepStrictEqual } from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import {
diff --git a/src/utils/queries/__tests__/index.test.mjs b/src/utils/queries/__tests__/index.test.mjs
index 60ab257a..3fcabcf1 100644
--- a/src/utils/queries/__tests__/index.test.mjs
+++ b/src/utils/queries/__tests__/index.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual, deepStrictEqual } from 'node:assert';
+import { strictEqual, deepStrictEqual } from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import typeMap from '../../parser/typeMap.json' with { type: 'json' };

From 4072e89ab00f8f4c880385d506d63d93e47c1266 Mon Sep 17 00:00:00 2001
From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com>
Date: Mon, 15 Dec 2025 11:46:12 +0100
Subject: [PATCH 6/6] cleanup

---
 src/__tests__/generators.test.mjs             | 46 +++++-----
 src/__tests__/metadata.test.mjs               | 13 +--
 src/__tests__/streaming.test.mjs              | 46 +++++-----
 .../utils/__tests__/converter.test.mjs        | 20 ++---
 .../metadata/__tests__/index.test.mjs         |  8 +-
 .../metadata/utils/__tests__/parse.test.mjs   | 28 +++---
 .../orama-db/__tests__/generate.test.mjs      | 20 +++--
 src/logger/__tests__/logger.test.mjs          | 88 +++++++++----------
 .../__tests__/transports/console.test.mjs     | 42 ++++-----
 .../__tests__/transports/github.test.mjs      | 30 +++----
 src/threading/__tests__/parallel.test.mjs     | 20 ++---
 src/utils/__tests__/unist.test.mjs            | 14 ++-
 src/utils/parser/__tests__/index.test.mjs     | 20 ++---
 src/utils/queries/__tests__/index.test.mjs    | 26 +++---
 14 files changed, 216 insertions(+), 205 deletions(-)

diff --git a/src/__tests__/generators.test.mjs b/src/__tests__/generators.test.mjs
index 56af8eff..9dfe1c09 100644
--- a/src/__tests__/generators.test.mjs
+++ b/src/__tests__/generators.test.mjs
@@ -1,4 +1,4 @@
-import { ok, strictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import createGenerator from '../generators.mjs';
@@ -21,8 +21,8 @@ describe('createGenerator', () => {
   it('should create a generator orchestrator with runGenerators method', () => {
     const { runGenerators } = createGenerator();
 
-    ok(runGenerators);
-    strictEqual(typeof runGenerators, 'function');
+    assert.ok(runGenerators);
+    assert.strictEqual(typeof runGenerators, 'function');
   });
 
   it('should return the ast input directly when generators list is empty', async () => {
@@ -34,9 +34,9 @@ describe('createGenerator', () => {
     });
 
     // Returns array of results, first element is the 'ast' result
-    ok(Array.isArray(results));
-    strictEqual(results.length, 1);
-    ok(results[0]);
+    assert.ok(Array.isArray(results));
+    assert.strictEqual(results.length, 1);
+    assert.ok(results[0]);
   });
 
   it('should run metadata generator', async () => {
@@ -48,9 +48,9 @@ describe('createGenerator', () => {
     });
 
     // Returns array with one element - the collected metadata array
-    ok(Array.isArray(results));
-    strictEqual(results.length, 1);
-    ok(Array.isArray(results[0]));
+    assert.ok(Array.isArray(results));
+    assert.strictEqual(results.length, 1);
+    assert.ok(Array.isArray(results[0]));
   });
 
   it('should handle generator with dependency', async () => {
@@ -63,8 +63,8 @@ describe('createGenerator', () => {
     });
 
     // Should complete without error - returns array of results
-    ok(Array.isArray(results));
-    strictEqual(results.length, 1);
+    assert.ok(Array.isArray(results));
+    assert.strictEqual(results.length, 1);
   });
 
   it('should skip already scheduled generators', async () => {
@@ -77,8 +77,8 @@ describe('createGenerator', () => {
     });
 
     // Returns array with two elements (same result cached for both)
-    ok(Array.isArray(results));
-    strictEqual(results.length, 2);
+    assert.ok(Array.isArray(results));
+    assert.strictEqual(results.length, 2);
   });
 
   it('should handle multiple generators in sequence', async () => {
@@ -91,8 +91,8 @@ describe('createGenerator', () => {
     });
 
     // Returns array of results
-    ok(Array.isArray(results));
-    strictEqual(results.length, 1);
+    assert.ok(Array.isArray(results));
+    assert.strictEqual(results.length, 1);
   });
 
   it('should collect async generator results for dependents', async () => {
@@ -104,8 +104,8 @@ describe('createGenerator', () => {
       generators: ['legacy-json'],
     });
 
-    ok(Array.isArray(results));
-    strictEqual(results.length, 1);
+    assert.ok(Array.isArray(results));
+    assert.strictEqual(results.length, 1);
   });
 
   it('should use multiple threads when specified', async () => {
@@ -118,9 +118,9 @@ describe('createGenerator', () => {
     });
 
     // Returns array of results
-    ok(Array.isArray(results));
-    strictEqual(results.length, 1);
-    ok(Array.isArray(results[0]));
+    assert.ok(Array.isArray(results));
+    assert.strictEqual(results.length, 1);
+    assert.ok(Array.isArray(results[0]));
   });
 
   it('should pass options to generators', async () => {
@@ -135,8 +135,8 @@ describe('createGenerator', () => {
     });
 
     // Returns array of results
-    ok(Array.isArray(results));
-    strictEqual(results.length, 1);
-    ok(Array.isArray(results[0]));
+    assert.ok(Array.isArray(results));
+    assert.strictEqual(results.length, 1);
+    assert.ok(Array.isArray(results[0]));
   });
 });
diff --git a/src/__tests__/metadata.test.mjs b/src/__tests__/metadata.test.mjs
index ec468ea3..4abf975b 100644
--- a/src/__tests__/metadata.test.mjs
+++ b/src/__tests__/metadata.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual, deepStrictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import GitHubSlugger from 'github-slugger';
@@ -21,7 +21,10 @@ describe('createMetadata', () => {
       },
     });
     metadata.setHeading(heading);
-    strictEqual(metadata.create(new VFile(), {}).heading.data, heading.data);
+    assert.strictEqual(
+      metadata.create(new VFile(), {}).heading.data,
+      heading.data
+    );
   });
 
   it('should set the stability correctly', () => {
@@ -34,7 +37,7 @@ describe('createMetadata', () => {
     };
     metadata.addStability(stability);
     const actual = metadata.create(new VFile(), {}).stability;
-    deepStrictEqual(actual, {
+    assert.deepStrictEqual(actual, {
       children: [stability],
       type: 'root',
     });
@@ -83,7 +86,7 @@ describe('createMetadata', () => {
       yaml_position: {},
     };
     const actual = metadata.create(apiDoc, section);
-    deepStrictEqual(actual, expected);
+    assert.deepStrictEqual(actual, expected);
   });
 
   it('should be serializable', () => {
@@ -92,6 +95,6 @@ describe('createMetadata', () => {
       type: 'root',
       children: [],
     });
-    deepStrictEqual(structuredClone(actual), actual);
+    assert.deepStrictEqual(structuredClone(actual), actual);
   });
 });
diff --git a/src/__tests__/streaming.test.mjs b/src/__tests__/streaming.test.mjs
index 23aa7a9d..190d1b8f 100644
--- a/src/__tests__/streaming.test.mjs
+++ b/src/__tests__/streaming.test.mjs
@@ -1,4 +1,4 @@
-import { deepStrictEqual, ok, strictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import {
@@ -16,7 +16,7 @@ describe('streaming utilities', () => {
 
       const gen = asyncGen();
 
-      strictEqual(isAsyncGenerator(gen), true);
+      assert.strictEqual(isAsyncGenerator(gen), true);
     });
 
     it('should return false for regular generators', () => {
@@ -26,24 +26,24 @@ describe('streaming utilities', () => {
 
       const gen = syncGen();
 
-      strictEqual(isAsyncGenerator(gen), false);
+      assert.strictEqual(isAsyncGenerator(gen), false);
     });
 
     it('should return false for plain objects', () => {
-      strictEqual(isAsyncGenerator({}), false);
-      strictEqual(isAsyncGenerator([]), false);
-      strictEqual(isAsyncGenerator({ async: true }), false);
+      assert.strictEqual(isAsyncGenerator({}), false);
+      assert.strictEqual(isAsyncGenerator([]), false);
+      assert.strictEqual(isAsyncGenerator({ async: true }), false);
     });
 
     it('should return false for null and undefined', () => {
-      strictEqual(isAsyncGenerator(null), false);
-      strictEqual(isAsyncGenerator(undefined), false);
+      assert.strictEqual(isAsyncGenerator(null), false);
+      assert.strictEqual(isAsyncGenerator(undefined), false);
     });
 
     it('should return false for primitives', () => {
-      strictEqual(isAsyncGenerator(42), false);
-      strictEqual(isAsyncGenerator('string'), false);
-      strictEqual(isAsyncGenerator(true), false);
+      assert.strictEqual(isAsyncGenerator(42), false);
+      assert.strictEqual(isAsyncGenerator('string'), false);
+      assert.strictEqual(isAsyncGenerator(true), false);
     });
 
     it('should return true for objects with Symbol.asyncIterator', () => {
@@ -55,7 +55,7 @@ describe('streaming utilities', () => {
         },
       };
 
-      strictEqual(isAsyncGenerator(asyncIterable), true);
+      assert.strictEqual(isAsyncGenerator(asyncIterable), true);
     });
   });
 
@@ -69,7 +69,7 @@ describe('streaming utilities', () => {
 
       const result = await collectAsyncGenerator(gen());
 
-      deepStrictEqual(result, [1, 2, 3, 4, 5]);
+      assert.deepStrictEqual(result, [1, 2, 3, 4, 5]);
     });
 
     it('should return empty array for empty generator', async () => {
@@ -79,7 +79,7 @@ describe('streaming utilities', () => {
 
       const result = await collectAsyncGenerator(gen());
 
-      deepStrictEqual(result, []);
+      assert.deepStrictEqual(result, []);
     });
 
     it('should handle single chunk', async () => {
@@ -89,7 +89,7 @@ describe('streaming utilities', () => {
 
       const result = await collectAsyncGenerator(gen());
 
-      deepStrictEqual(result, [1, 2, 3]);
+      assert.deepStrictEqual(result, [1, 2, 3]);
     });
 
     it('should handle empty chunks', async () => {
@@ -102,7 +102,7 @@ describe('streaming utilities', () => {
 
       const result = await collectAsyncGenerator(gen());
 
-      deepStrictEqual(result, [1, 2, 3]);
+      assert.deepStrictEqual(result, [1, 2, 3]);
     });
 
     it('should handle objects in chunks', async () => {
@@ -113,7 +113,7 @@ describe('streaming utilities', () => {
 
       const result = await collectAsyncGenerator(gen());
 
-      deepStrictEqual(result, [{ a: 1 }, { b: 2 }, { c: 3 }]);
+      assert.deepStrictEqual(result, [{ a: 1 }, { b: 2 }, { c: 3 }]);
     });
   });
 
@@ -121,8 +121,8 @@ describe('streaming utilities', () => {
     it('should create a cache with required methods', () => {
       const cache = createStreamingCache();
 
-      ok(cache);
-      strictEqual(typeof cache.getOrCollect, 'function');
+      assert.ok(cache);
+      assert.strictEqual(typeof cache.getOrCollect, 'function');
     });
 
     it('should return same promise for same key', async () => {
@@ -145,8 +145,8 @@ describe('streaming utilities', () => {
       const result1 = await promise1;
       const result2 = await promise2;
 
-      deepStrictEqual(result1, [1, 2, 3]);
-      strictEqual(result1, result2);
+      assert.deepStrictEqual(result1, [1, 2, 3]);
+      assert.strictEqual(result1, result2);
     });
 
     it('should return different results for different keys', async () => {
@@ -163,8 +163,8 @@ describe('streaming utilities', () => {
       const result1 = await cache.getOrCollect('key1', gen1());
       const result2 = await cache.getOrCollect('key2', gen2());
 
-      deepStrictEqual(result1, [1, 2]);
-      deepStrictEqual(result2, [3, 4]);
+      assert.deepStrictEqual(result1, [1, 2]);
+      assert.deepStrictEqual(result2, [3, 4]);
     });
   });
 });
diff --git a/src/generators/man-page/utils/__tests__/converter.test.mjs b/src/generators/man-page/utils/__tests__/converter.test.mjs
index bec35edd..1c6c7884 100644
--- a/src/generators/man-page/utils/__tests__/converter.test.mjs
+++ b/src/generators/man-page/utils/__tests__/converter.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import { u } from 'unist-builder';
@@ -20,7 +20,7 @@ const createMockElement = (headingText, description) => ({
 
 const runTests = (cases, conversionFunc) => {
   cases.forEach(({ input, expected }) => {
-    strictEqual(conversionFunc(input), expected);
+    assert.strictEqual(conversionFunc(input), expected);
   });
 };
 
@@ -31,7 +31,7 @@ describe('Mandoc Conversion', () => {
         u('heading', { depth: 1 }, [textNode('Main Title')]),
         u('paragraph', [textNode('Introductory text.')]),
       ]);
-      strictEqual(
+      assert.strictEqual(
         convertNodeToMandoc(node),
         '.Sh Main Title\nIntroductory text.'
       );
@@ -111,7 +111,7 @@ describe('Mandoc Conversion', () => {
         '`-a`, `-b=value`',
         'Description of the options.'
       );
-      strictEqual(
+      assert.strictEqual(
         convertOptionToMandoc(mockElement),
         `.It Fl a , Fl b Ns = Ns Ar value\nDescription of the options.\n.\n`
       );
@@ -122,7 +122,7 @@ describe('Mandoc Conversion', () => {
         '`-a`',
         'Description of the option without a value.'
       );
-      strictEqual(
+      assert.strictEqual(
         convertOptionToMandoc(mockElement),
         `.It Fl a\nDescription of the option without a value.\n.\n`
       );
@@ -133,7 +133,7 @@ describe('Mandoc Conversion', () => {
         '`-x`, `-y`, `-z=value`',
         'Description of multiple options.'
       );
-      strictEqual(
+      assert.strictEqual(
         convertOptionToMandoc(mockElement),
         `.It Fl x , Fl y , Fl z Ns = Ns Ar value\nDescription of multiple options.\n.\n`
       );
@@ -144,7 +144,7 @@ describe('Mandoc Conversion', () => {
         '`-d`, `--option=value with spaces`',
         'Description of special options.'
       );
-      strictEqual(
+      assert.strictEqual(
         convertOptionToMandoc(mockElement),
         `.It Fl d , Fl -option Ns = Ns Ar value with spaces\nDescription of special options.\n.\n`
       );
@@ -157,7 +157,7 @@ describe('Mandoc Conversion', () => {
         '`MY_VAR=some_value`',
         'Description of the environment variable.'
       );
-      strictEqual(
+      assert.strictEqual(
         convertEnvVarToMandoc(mockElement),
         `.It Ev MY_VAR Ar some_value\nDescription of the environment variable.\n.\n`
       );
@@ -168,7 +168,7 @@ describe('Mandoc Conversion', () => {
         '`MY_VAR=`',
         'Description of the environment variable without a value.'
       );
-      strictEqual(
+      assert.strictEqual(
         convertEnvVarToMandoc(mockElement),
         `.It Ev MY_VAR\nDescription of the environment variable without a value.\n.\n`
       );
@@ -179,7 +179,7 @@ describe('Mandoc Conversion', () => {
         '`SPECIAL_VAR=special value!`',
         'Description of special environment variable.'
       );
-      strictEqual(
+      assert.strictEqual(
         convertEnvVarToMandoc(mockElement),
         `.It Ev SPECIAL_VAR Ar special value!\nDescription of special environment variable.\n.\n`
       );
diff --git a/src/generators/metadata/__tests__/index.test.mjs b/src/generators/metadata/__tests__/index.test.mjs
index 41ef27af..7d0695e4 100644
--- a/src/generators/metadata/__tests__/index.test.mjs
+++ b/src/generators/metadata/__tests__/index.test.mjs
@@ -1,4 +1,4 @@
-import { deepStrictEqual, strictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import generator from '../index.mjs';
@@ -24,8 +24,8 @@ describe('generators/metadata/index', () => {
       results.push(chunk);
     }
 
-    strictEqual(results.length, 2);
-    deepStrictEqual(results[0], [1, 2, 3]);
-    deepStrictEqual(results[1], [4]);
+    assert.strictEqual(results.length, 2);
+    assert.deepStrictEqual(results[0], [1, 2, 3]);
+    assert.deepStrictEqual(results[1], [4]);
   });
 });
diff --git a/src/generators/metadata/utils/__tests__/parse.test.mjs b/src/generators/metadata/utils/__tests__/parse.test.mjs
index b2701cca..0185b2a5 100644
--- a/src/generators/metadata/utils/__tests__/parse.test.mjs
+++ b/src/generators/metadata/utils/__tests__/parse.test.mjs
@@ -1,4 +1,4 @@
-import { deepStrictEqual, strictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import { u } from 'unist-builder';
@@ -22,17 +22,17 @@ describe('parseApiDoc', () => {
 
     const results = parseApiDoc({ file, tree }, {});
 
-    strictEqual(results.length, 1);
+    assert.strictEqual(results.length, 1);
     const [entry] = results;
 
-    strictEqual(entry.source_link, 'https://example.com');
-    strictEqual(entry.stability.children.length, 1);
-    strictEqual(entry.stability.children[0].data.index, '2');
+    assert.strictEqual(entry.source_link, 'https://example.com');
+    assert.strictEqual(entry.stability.children.length, 1);
+    assert.strictEqual(entry.stability.children[0].data.index, '2');
 
     // Find a paragraph child that contains a link and assert transformed URL
     const paragraph = entry.content.children.find(n => n.type === 'paragraph');
     const link = paragraph.children.find(c => c.type === 'link');
-    strictEqual(link.url, 'other.html#foo');
+    assert.strictEqual(link.url, 'other.html#foo');
   });
 
   it('inserts a fake heading when none exist', () => {
@@ -41,11 +41,11 @@ describe('parseApiDoc', () => {
 
     const results = parseApiDoc({ file, tree }, {});
 
-    strictEqual(results.length, 1);
+    assert.strictEqual(results.length, 1);
     const [entry] = results;
 
     // Fake heading has empty text
-    deepStrictEqual(entry.heading.data.text, '');
+    assert.deepStrictEqual(entry.heading.data.text, '');
   });
 
   it('converts link references using definitions and removes definitions', () => {
@@ -71,12 +71,12 @@ describe('parseApiDoc', () => {
 
     const results = parseApiDoc({ file, tree }, {});
 
-    strictEqual(results.length, 1);
+    assert.strictEqual(results.length, 1);
     const [entry] = results;
 
     const paragraph = entry.content.children.find(n => n.type === 'paragraph');
     const link = paragraph.children.find(c => c.type === 'link');
-    strictEqual(link.url, 'https://def.example/');
+    assert.strictEqual(link.url, 'https://def.example/');
   });
 
   it('converts type references to links using provided typeMap', () => {
@@ -89,12 +89,12 @@ describe('parseApiDoc', () => {
 
     const results = parseApiDoc({ file, tree }, { Foo: 'foo.html' });
 
-    strictEqual(results.length, 1);
+    assert.strictEqual(results.length, 1);
     const [entry] = results;
 
     const paragraph = entry.content.children.find(n => n.type === 'paragraph');
     const link = paragraph.children.find(c => c.type === 'link');
-    strictEqual(link.url, 'foo.html');
+    assert.strictEqual(link.url, 'foo.html');
   });
 
   it('converts unix manual references to man7 links', () => {
@@ -107,12 +107,12 @@ describe('parseApiDoc', () => {
 
     const results = parseApiDoc({ file, tree }, {});
 
-    strictEqual(results.length, 1);
+    assert.strictEqual(results.length, 1);
     const [entry] = results;
 
     const paragraph = entry.content.children.find(n => n.type === 'paragraph');
     const link = paragraph.children.find(c => c.type === 'link');
     // should point to man7 man page for ls in section 1
-    strictEqual(link.url.includes('man-pages/man1/ls.1.html'), true);
+    assert.strictEqual(link.url.includes('man-pages/man1/ls.1.html'), true);
   });
 });
diff --git a/src/generators/orama-db/__tests__/generate.test.mjs b/src/generators/orama-db/__tests__/generate.test.mjs
index 6273db4c..6ea78586 100644
--- a/src/generators/orama-db/__tests__/generate.test.mjs
+++ b/src/generators/orama-db/__tests__/generate.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual, rejects, ok } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { mkdtemp, readFile, rm } from 'node:fs/promises';
 import { tmpdir } from 'node:os';
 import { join } from 'node:path';
@@ -8,10 +8,12 @@ import generator from '../index.mjs';
 
 describe('orama-db generate', () => {
   it('throws when input is missing or empty', async () => {
-    await rejects(async () =>
+    await assert.rejects(async () =>
       generator.generate(undefined, { output: './tmp' })
     );
-    await rejects(async () => generator.generate([], { output: './tmp' }));
+    await assert.rejects(async () =>
+      generator.generate([], { output: './tmp' })
+    );
   });
 
   it('throws when output path is missing', async () => {
@@ -23,7 +25,7 @@ describe('orama-db generate', () => {
         content: { children: [] },
       },
     ];
-    await rejects(async () =>
+    await assert.rejects(async () =>
       generator.generate(fakeInput, { output: undefined })
     );
   });
@@ -69,14 +71,14 @@ describe('orama-db generate', () => {
       const parsed = JSON.parse(file);
 
       // Basic sanity checks on saved DB structure
-      ok(parsed, 'saved DB should be JSON');
+      assert.ok(parsed, 'saved DB should be JSON');
       // Expect some representation of documents to exist
       // The exact schema is internal to orama; ensure serialized contains our slugs/titles
       const serialized = JSON.stringify(parsed);
-      strictEqual(serialized.includes('mymod.html#one'), true);
-      strictEqual(serialized.includes('mymod.html#two'), true);
-      strictEqual(serialized.includes('Module'), true);
-      strictEqual(serialized.includes('Child'), true);
+      assert.strictEqual(serialized.includes('mymod.html#one'), true);
+      assert.strictEqual(serialized.includes('mymod.html#two'), true);
+      assert.strictEqual(serialized.includes('Module'), true);
+      assert.strictEqual(serialized.includes('Child'), true);
     } finally {
       await rm(dir, { recursive: true, force: true });
     }
diff --git a/src/logger/__tests__/logger.test.mjs b/src/logger/__tests__/logger.test.mjs
index da12fb51..633459a9 100644
--- a/src/logger/__tests__/logger.test.mjs
+++ b/src/logger/__tests__/logger.test.mjs
@@ -1,4 +1,4 @@
-import { deepStrictEqual, strictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import { LogLevel } from '../constants.mjs';
@@ -29,10 +29,10 @@ describe('createLogger', () => {
 
       logger.debug('Hello, World!', metadata);
 
-      strictEqual(transport.mock.callCount(), 1);
+      assert.strictEqual(transport.mock.callCount(), 1);
 
       const call = transport.mock.calls[0];
-      deepStrictEqual(call.arguments, [
+      assert.deepStrictEqual(call.arguments, [
         {
           level: LogLevel.debug,
           message: 'Hello, World!',
@@ -52,7 +52,7 @@ describe('createLogger', () => {
 
           logger.debug('Hello, World!');
 
-          strictEqual(transport.mock.callCount(), 0);
+          assert.strictEqual(transport.mock.callCount(), 0);
         }
       );
     });
@@ -68,10 +68,10 @@ describe('createLogger', () => {
 
         logger.info('Hello, World!', metadata);
 
-        strictEqual(transport.mock.callCount(), 1);
+        assert.strictEqual(transport.mock.callCount(), 1);
 
         const call = transport.mock.calls[0];
-        deepStrictEqual(call.arguments, [
+        assert.deepStrictEqual(call.arguments, [
           {
             level: LogLevel.info,
             message: 'Hello, World!',
@@ -91,7 +91,7 @@ describe('createLogger', () => {
 
         logger.info('Hello, World!');
 
-        strictEqual(transport.mock.callCount(), 0);
+        assert.strictEqual(transport.mock.callCount(), 0);
       });
     });
   });
@@ -107,10 +107,10 @@ describe('createLogger', () => {
 
         logger.warn('Hello, World!', metadata);
 
-        strictEqual(transport.mock.callCount(), 1);
+        assert.strictEqual(transport.mock.callCount(), 1);
 
         const call = transport.mock.calls[0];
-        deepStrictEqual(call.arguments, [
+        assert.deepStrictEqual(call.arguments, [
           {
             level: LogLevel.warn,
             message: 'Hello, World!',
@@ -130,7 +130,7 @@ describe('createLogger', () => {
 
         logger.warn('Hello, World!');
 
-        strictEqual(transport.mock.callCount(), 0);
+        assert.strictEqual(transport.mock.callCount(), 0);
       });
     });
   });
@@ -147,10 +147,10 @@ describe('createLogger', () => {
 
           logger.error('Hello, World!', metadata);
 
-          strictEqual(transport.mock.callCount(), 1);
+          assert.strictEqual(transport.mock.callCount(), 1);
 
           const call = transport.mock.calls[0];
-          deepStrictEqual(call.arguments, [
+          assert.deepStrictEqual(call.arguments, [
             {
               level: LogLevel.error,
               message: 'Hello, World!',
@@ -170,7 +170,7 @@ describe('createLogger', () => {
 
       logger.warn('Hello, World!');
 
-      strictEqual(transport.mock.callCount(), 0);
+      assert.strictEqual(transport.mock.callCount(), 0);
     });
   });
 
@@ -184,7 +184,7 @@ describe('createLogger', () => {
       logger[level]('Hello, World!');
     });
 
-    strictEqual(transport.mock.callCount(), 0);
+    assert.strictEqual(transport.mock.callCount(), 0);
   });
 
   it('should log all messages if message is a string array', t => {
@@ -194,7 +194,7 @@ describe('createLogger', () => {
 
     logger.info(['Hello, 1!', 'Hello, 2!', 'Hello, 3!']);
 
-    strictEqual(transport.mock.callCount(), 3);
+    assert.strictEqual(transport.mock.callCount(), 3);
   });
 
   it('should log error message', t => {
@@ -207,10 +207,10 @@ describe('createLogger', () => {
     const error = new Error('Hello, World!');
     logger.error(error);
 
-    strictEqual(transport.mock.callCount(), 1);
+    assert.strictEqual(transport.mock.callCount(), 1);
 
     const call = transport.mock.calls[0];
-    deepStrictEqual(call.arguments, [
+    assert.deepStrictEqual(call.arguments, [
       {
         level: LogLevel.error,
         message: 'Hello, World!',
@@ -231,18 +231,18 @@ describe('createLogger', () => {
 
       // Should log at info level
       logger.info('Info message');
-      strictEqual(transport.mock.callCount(), 1);
+      assert.strictEqual(transport.mock.callCount(), 1);
 
       // Change to error level
       logger.setLogLevel(LogLevel.error);
 
       // Should not log info anymore
       logger.info('Another info message');
-      strictEqual(transport.mock.callCount(), 1);
+      assert.strictEqual(transport.mock.callCount(), 1);
 
       // Should log error
       logger.error('Error message');
-      strictEqual(transport.mock.callCount(), 2);
+      assert.strictEqual(transport.mock.callCount(), 2);
     });
 
     it('should change log level at runtime using string', t => {
@@ -252,14 +252,14 @@ describe('createLogger', () => {
 
       // Should not log at info level initially
       logger.info('Info message');
-      strictEqual(transport.mock.callCount(), 0);
+      assert.strictEqual(transport.mock.callCount(), 0);
 
       // Change to debug level using string
       logger.setLogLevel('debug');
 
       // Should now log info
       logger.info('Another info message');
-      strictEqual(transport.mock.callCount(), 1);
+      assert.strictEqual(transport.mock.callCount(), 1);
     });
 
     it('should handle case-insensitive level names', t => {
@@ -269,11 +269,11 @@ describe('createLogger', () => {
 
       logger.setLogLevel('DEBUG');
       logger.debug('Debug message');
-      strictEqual(transport.mock.callCount(), 1);
+      assert.strictEqual(transport.mock.callCount(), 1);
 
       logger.setLogLevel('Info');
       logger.debug('Debug message 2');
-      strictEqual(transport.mock.callCount(), 1); // Should not log debug at info level
+      assert.strictEqual(transport.mock.callCount(), 1); // Should not log debug at info level
     });
 
     it('should propagate to child loggers', t => {
@@ -284,28 +284,28 @@ describe('createLogger', () => {
 
       // Child should initially respect parent's info level
       child.debug('Debug message');
-      strictEqual(transport.mock.callCount(), 0);
+      assert.strictEqual(transport.mock.callCount(), 0);
 
       child.info('Info message');
-      strictEqual(transport.mock.callCount(), 1);
+      assert.strictEqual(transport.mock.callCount(), 1);
 
       // Change parent to debug level
       logger.setLogLevel(LogLevel.debug);
 
       // Child should now log debug messages
       child.debug('Debug message after level change');
-      strictEqual(transport.mock.callCount(), 2);
+      assert.strictEqual(transport.mock.callCount(), 2);
 
       // Change parent to error level
       logger.setLogLevel(LogLevel.error);
 
       // Child should not log info anymore
       child.info('Info message after error level');
-      strictEqual(transport.mock.callCount(), 2);
+      assert.strictEqual(transport.mock.callCount(), 2);
 
       // Child should log error
       child.error('Error message');
-      strictEqual(transport.mock.callCount(), 3);
+      assert.strictEqual(transport.mock.callCount(), 3);
     });
 
     it('should propagate to nested child loggers', t => {
@@ -321,20 +321,20 @@ describe('createLogger', () => {
       child1.debug('child1 debug');
       child2.debug('child2 debug');
       child3.debug('child3 debug');
-      strictEqual(transport.mock.callCount(), 0);
+      assert.strictEqual(transport.mock.callCount(), 0);
 
       // Change root to debug level
       logger.setLogLevel(LogLevel.debug);
 
       // All should now log debug
       child1.debug('child1 debug after');
-      strictEqual(transport.mock.callCount(), 1);
+      assert.strictEqual(transport.mock.callCount(), 1);
 
       child2.debug('child2 debug after');
-      strictEqual(transport.mock.callCount(), 2);
+      assert.strictEqual(transport.mock.callCount(), 2);
 
       child3.debug('child3 debug after');
-      strictEqual(transport.mock.callCount(), 3);
+      assert.strictEqual(transport.mock.callCount(), 3);
     });
 
     it('should propagate to multiple children at same level', t => {
@@ -349,20 +349,20 @@ describe('createLogger', () => {
       childA.info('A info');
       childB.info('B info');
       childC.info('C info');
-      strictEqual(transport.mock.callCount(), 0);
+      assert.strictEqual(transport.mock.callCount(), 0);
 
       // Change root to info
       logger.setLogLevel(LogLevel.info);
 
       // All children should now log info
       childA.info('A info after');
-      strictEqual(transport.mock.callCount(), 1);
+      assert.strictEqual(transport.mock.callCount(), 1);
 
       childB.info('B info after');
-      strictEqual(transport.mock.callCount(), 2);
+      assert.strictEqual(transport.mock.callCount(), 2);
 
       childC.info('C info after');
-      strictEqual(transport.mock.callCount(), 3);
+      assert.strictEqual(transport.mock.callCount(), 3);
     });
 
     it('should ignore invalid string level names', t => {
@@ -375,24 +375,24 @@ describe('createLogger', () => {
 
       // Should still log at info level
       logger.info('Info message');
-      strictEqual(transport.mock.callCount(), 1);
+      assert.strictEqual(transport.mock.callCount(), 1);
 
       logger.debug('Debug message');
-      strictEqual(transport.mock.callCount(), 1); // Debug should be filtered
+      assert.strictEqual(transport.mock.callCount(), 1); // Debug should be filtered
     });
   });
 });
 
 describe('logger (smoke)', () => {
   it('exports a default logger instance', () => {
-    strictEqual(typeof logger.info, 'function');
-    strictEqual(typeof logger.error, 'function');
-    strictEqual(typeof logger.setLogLevel, 'function');
+    assert.strictEqual(typeof logger.info, 'function');
+    assert.strictEqual(typeof logger.error, 'function');
+    assert.strictEqual(typeof logger.setLogLevel, 'function');
   });
 
   it('can create a console logger via Logger()', () => {
     const consoleLogger = Logger('console');
-    strictEqual(typeof consoleLogger.info, 'function');
-    strictEqual(typeof consoleLogger.setLogLevel, 'function');
+    assert.strictEqual(typeof consoleLogger.info, 'function');
+    assert.strictEqual(typeof consoleLogger.setLogLevel, 'function');
   });
 });
diff --git a/src/logger/__tests__/transports/console.test.mjs b/src/logger/__tests__/transports/console.test.mjs
index 0cf3bf14..a1fbbe15 100644
--- a/src/logger/__tests__/transports/console.test.mjs
+++ b/src/logger/__tests__/transports/console.test.mjs
@@ -1,4 +1,4 @@
-import { deepStrictEqual, strictEqual } from 'assert';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import { LogLevel } from '../../constants.mjs';
@@ -25,8 +25,8 @@ describe('console', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 4);
-    deepStrictEqual(callsArgs, [
+    assert.strictEqual(process.stdout.write.mock.callCount(), 4);
+    assert.deepStrictEqual(callsArgs, [
       '[00:00:00.000]',
       ' \x1B[34mDEBUG\x1B[39m',
       ': Test message',
@@ -53,8 +53,8 @@ describe('console', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 4);
-    deepStrictEqual(callsArgs, [
+    assert.strictEqual(process.stdout.write.mock.callCount(), 4);
+    assert.deepStrictEqual(callsArgs, [
       '[00:00:00.000]',
       ' \x1B[32mINFO\x1B[39m',
       ': Test message',
@@ -82,8 +82,8 @@ describe('console', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 4);
-    deepStrictEqual(callsArgs, [
+    assert.strictEqual(process.stdout.write.mock.callCount(), 4);
+    assert.deepStrictEqual(callsArgs, [
       '[00:00:00.000]',
       ' \x1B[35mERROR\x1B[39m',
       ': Test message',
@@ -111,8 +111,8 @@ describe('console', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 4);
-    deepStrictEqual(callsArgs, [
+    assert.strictEqual(process.stdout.write.mock.callCount(), 4);
+    assert.deepStrictEqual(callsArgs, [
       '[00:00:00.000]',
       ' \x1B[31mFATAL\x1B[39m',
       ': Test message',
@@ -149,8 +149,8 @@ describe('console', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 6);
-    deepStrictEqual(callsArgs, [
+    assert.strictEqual(process.stdout.write.mock.callCount(), 6);
+    assert.deepStrictEqual(callsArgs, [
       '[00:00:00.000]',
       ' \x1B[32mINFO\x1B[39m',
       ': Test message',
@@ -181,8 +181,8 @@ describe('console', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 5);
-    deepStrictEqual(callsArgs, [
+    assert.strictEqual(process.stdout.write.mock.callCount(), 5);
+    assert.deepStrictEqual(callsArgs, [
       '[00:00:00.000]',
       ' \x1B[32mINFO\x1B[39m',
       ' (child1)',
@@ -211,8 +211,8 @@ describe('console', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 4);
-    deepStrictEqual(callsArgs, [
+    assert.strictEqual(process.stdout.write.mock.callCount(), 4);
+    assert.deepStrictEqual(callsArgs, [
       '[00:00:00.000]',
       ' INFO',
       ': Test message',
@@ -243,8 +243,8 @@ describe('console', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 5);
-    deepStrictEqual(callsArgs, [
+    assert.strictEqual(process.stdout.write.mock.callCount(), 5);
+    assert.deepStrictEqual(callsArgs, [
       '[00:00:00.000]',
       ' \x1B[32mINFO\x1B[39m',
       ': Test message',
@@ -280,8 +280,8 @@ describe('console', () => {
 
     // Should have: timestamp, level, message, file path, newline, stack
     // But NOT a metadata JSON block (since only file/stack are present)
-    strictEqual(process.stdout.write.mock.callCount(), 6);
-    deepStrictEqual(callsArgs, [
+    assert.strictEqual(process.stdout.write.mock.callCount(), 6);
+    assert.deepStrictEqual(callsArgs, [
       '[00:00:00.000]',
       ' \x1B[32mINFO\x1B[39m',
       ': Test message',
@@ -321,8 +321,8 @@ describe('console', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 7);
-    deepStrictEqual(callsArgs, [
+    assert.strictEqual(process.stdout.write.mock.callCount(), 7);
+    assert.deepStrictEqual(callsArgs, [
       '[00:00:00.000]',
       ' \x1B[34mDEBUG\x1B[39m',
       ': Processing chunk',
diff --git a/src/logger/__tests__/transports/github.test.mjs b/src/logger/__tests__/transports/github.test.mjs
index ed7d7dfa..8d2e44c1 100644
--- a/src/logger/__tests__/transports/github.test.mjs
+++ b/src/logger/__tests__/transports/github.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual } from 'assert';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import { LogLevel } from '../../constants.mjs';
@@ -25,8 +25,8 @@ describe('github', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 1);
-    strictEqual(
+    assert.strictEqual(process.stdout.write.mock.callCount(), 1);
+    assert.strictEqual(
       callsArgs[0].trim(),
       '::debug::[00:00:00.000] \x1B[34mDEBUG\x1B[39m: Test message'
     );
@@ -51,8 +51,8 @@ describe('github', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 1);
-    strictEqual(
+    assert.strictEqual(process.stdout.write.mock.callCount(), 1);
+    assert.strictEqual(
       callsArgs[0].trim(),
       '::notice ::[00:00:00.000] \x1B[32mINFO\x1B[39m: Test message'
     );
@@ -78,8 +78,8 @@ describe('github', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 1);
-    strictEqual(
+    assert.strictEqual(process.stdout.write.mock.callCount(), 1);
+    assert.strictEqual(
       callsArgs[0].trim(),
       '::error ::[00:00:00.000] \x1B[35mERROR\x1B[39m: Test message'
     );
@@ -105,8 +105,8 @@ describe('github', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 1);
-    strictEqual(
+    assert.strictEqual(process.stdout.write.mock.callCount(), 1);
+    assert.strictEqual(
       callsArgs[0].trim(),
       '::error ::[00:00:00.000] \x1B[31mFATAL\x1B[39m: Test message'
     );
@@ -141,8 +141,8 @@ describe('github', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 1);
-    strictEqual(
+    assert.strictEqual(process.stdout.write.mock.callCount(), 1);
+    assert.strictEqual(
       callsArgs[0].trim(),
       '::notice file=test.md,line=1,endLine=1::[00:00:00.000] \x1B[32mINFO\x1B[39m: Test message'
     );
@@ -168,8 +168,8 @@ describe('github', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 1);
-    strictEqual(
+    assert.strictEqual(process.stdout.write.mock.callCount(), 1);
+    assert.strictEqual(
       callsArgs[0].trim(),
       '::notice ::[00:00:00.000] \x1B[32mINFO\x1B[39m (child1): Test message'
     );
@@ -195,8 +195,8 @@ describe('github', () => {
       call => call.arguments[0]
     );
 
-    strictEqual(process.stdout.write.mock.callCount(), 1);
-    strictEqual(
+    assert.strictEqual(process.stdout.write.mock.callCount(), 1);
+    assert.strictEqual(
       callsArgs[0].trim(),
       '::notice ::[00:00:00.000] INFO: Test message'
     );
diff --git a/src/threading/__tests__/parallel.test.mjs b/src/threading/__tests__/parallel.test.mjs
index 53622617..91782e86 100644
--- a/src/threading/__tests__/parallel.test.mjs
+++ b/src/threading/__tests__/parallel.test.mjs
@@ -1,4 +1,4 @@
-import { deepStrictEqual, ok, strictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import createWorkerPool from '../index.mjs';
@@ -43,8 +43,8 @@ describe('createParallelWorker', () => {
     const pool = createWorkerPool(2);
     const worker = createParallelWorker('metadata', pool, { threads: 2 });
 
-    ok(worker);
-    strictEqual(typeof worker.stream, 'function');
+    assert.ok(worker);
+    assert.strictEqual(typeof worker.stream, 'function');
 
     await pool.destroy();
   });
@@ -58,7 +58,7 @@ describe('createParallelWorker', () => {
 
     const results = await collectStream(worker.stream([], [], {}));
 
-    deepStrictEqual(results, []);
+    assert.deepStrictEqual(results, []);
 
     await pool.destroy();
   });
@@ -93,10 +93,10 @@ describe('createParallelWorker', () => {
       worker.stream(mockInput, mockInput, { typeMap: {} })
     );
 
-    strictEqual(chunks.length, 4);
+    assert.strictEqual(chunks.length, 4);
 
     for (const chunk of chunks) {
-      ok(Array.isArray(chunk));
+      assert.ok(Array.isArray(chunk));
     }
 
     await pool.destroy();
@@ -124,7 +124,7 @@ describe('createParallelWorker', () => {
       worker.stream(mockInput, mockInput, { typeMap: {} })
     );
 
-    strictEqual(chunks.length, 2);
+    assert.strictEqual(chunks.length, 2);
 
     await pool.destroy();
   });
@@ -147,8 +147,8 @@ describe('createParallelWorker', () => {
       worker.stream(mockInput, mockInput, { typeMap: {} })
     );
 
-    strictEqual(chunks.length, 1);
-    ok(Array.isArray(chunks[0]));
+    assert.strictEqual(chunks.length, 1);
+    assert.ok(Array.isArray(chunks[0]));
 
     await pool.destroy();
   });
@@ -175,7 +175,7 @@ describe('createParallelWorker', () => {
       worker.stream(mockInput, mockInput, { typeMap: {} })
     );
 
-    strictEqual(chunks.length, 2);
+    assert.strictEqual(chunks.length, 2);
 
     await pool.destroy();
   });
diff --git a/src/utils/__tests__/unist.test.mjs b/src/utils/__tests__/unist.test.mjs
index ead77fe8..33eee580 100644
--- a/src/utils/__tests__/unist.test.mjs
+++ b/src/utils/__tests__/unist.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import { transformNodesToString, callIfBefore } from '../unist.mjs';
@@ -25,7 +25,7 @@ describe('transformNodesToString', () => {
         ],
       },
     ];
-    strictEqual(transformNodesToString(nodes), 'Hello, **World**');
+    assert.strictEqual(transformNodesToString(nodes), 'Hello, **World**');
   });
 });
 
@@ -46,7 +46,10 @@ describe('callIfBefore', () => {
       },
     };
     callIfBefore(nodeA, nodeB, (nodeA, nodeB) => {
-      strictEqual(nodeA.position.start.line < nodeB.position.start.line, true);
+      assert.strictEqual(
+        nodeA.position.start.line < nodeB.position.start.line,
+        true
+      );
     });
   });
 
@@ -66,7 +69,10 @@ describe('callIfBefore', () => {
       },
     };
     callIfBefore(nodeA, nodeB, (nodeA, nodeB) => {
-      strictEqual(nodeA.position.start.line < nodeB.position.start.line, true);
+      assert.strictEqual(
+        nodeA.position.start.line < nodeB.position.start.line,
+        true
+      );
     });
   });
 });
diff --git a/src/utils/parser/__tests__/index.test.mjs b/src/utils/parser/__tests__/index.test.mjs
index 3c8f59f3..1620bdcb 100644
--- a/src/utils/parser/__tests__/index.test.mjs
+++ b/src/utils/parser/__tests__/index.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual, deepStrictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import {
@@ -10,21 +10,21 @@ import {
 
 describe('transformTypeToReferenceLink', () => {
   it('should transform a JavaScript primitive type into a Markdown link', () => {
-    strictEqual(
+    assert.strictEqual(
       transformTypeToReferenceLink('string'),
       '[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type)'
     );
   });
 
   it('should transform a JavaScript global type into a Markdown link', () => {
-    strictEqual(
+    assert.strictEqual(
       transformTypeToReferenceLink('Array'),
       '[``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)'
     );
   });
 
   it('should transform a type into a Markdown link', () => {
-    strictEqual(
+    assert.strictEqual(
       transformTypeToReferenceLink('SomeOtherType', {
         SomeOtherType: 'fromTypeMap',
       }),
@@ -43,7 +43,7 @@ llm_description=This is a test module`;
 
     const normalizedYaml = normalizeYamlSyntax(input);
 
-    strictEqual(
+    assert.strictEqual(
       normalizedYaml,
       `introduced_in: v0.1.21
 source_link: lib/test.js
@@ -58,7 +58,7 @@ llm_description: This is a test module`
 
     const normalizedYaml = normalizeYamlSyntax(input);
 
-    strictEqual(normalizedYaml, 'introduced_in: v0.1.21');
+    assert.strictEqual(normalizedYaml, 'introduced_in: v0.1.21');
   });
 });
 
@@ -70,7 +70,7 @@ describe('parseYAMLIntoMetadata', () => {
       type: 'module',
       introduced_in: 'v1.0.0',
     };
-    deepStrictEqual(parseYAMLIntoMetadata(input), expectedOutput);
+    assert.deepStrictEqual(parseYAMLIntoMetadata(input), expectedOutput);
   });
 
   it('should parse a YAML string with multiple versions into a JavaScript object', () => {
@@ -80,7 +80,7 @@ describe('parseYAMLIntoMetadata', () => {
       type: 'module',
       introduced_in: ['v1.0.0', 'v1.1.0'],
     };
-    deepStrictEqual(parseYAMLIntoMetadata(input), expectedOutput);
+    assert.deepStrictEqual(parseYAMLIntoMetadata(input), expectedOutput);
   });
 
   it('should parse a YAML string with source_link into a JavaScript object', () => {
@@ -92,7 +92,7 @@ describe('parseYAMLIntoMetadata', () => {
       introduced_in: 'v1.0.0',
       source_link: 'https://github.com/nodejs/node',
     };
-    deepStrictEqual(parseYAMLIntoMetadata(input), expectedOutput);
+    assert.deepStrictEqual(parseYAMLIntoMetadata(input), expectedOutput);
   });
 
   it('should parse a raw Heading string into Heading metadata', () => {
@@ -102,6 +102,6 @@ describe('parseYAMLIntoMetadata', () => {
       name: '## test',
       depth: 2,
     };
-    deepStrictEqual(parseHeadingIntoMetadata(input, 2), expectedOutput);
+    assert.deepStrictEqual(parseHeadingIntoMetadata(input, 2), expectedOutput);
   });
 });
diff --git a/src/utils/queries/__tests__/index.test.mjs b/src/utils/queries/__tests__/index.test.mjs
index 3fcabcf1..df647a33 100644
--- a/src/utils/queries/__tests__/index.test.mjs
+++ b/src/utils/queries/__tests__/index.test.mjs
@@ -1,4 +1,4 @@
-import { strictEqual, deepStrictEqual } from 'node:assert/strict';
+import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 
 import typeMap from '../../parser/typeMap.json' with { type: 'json' };
@@ -11,7 +11,7 @@ describe('createQueries', () => {
     const node = { value: 'type: test\nname: test\n' };
     const apiEntryMetadata = {
       updateProperties: properties => {
-        deepStrictEqual(properties, { type: 'test', name: 'test' });
+        assert.deepStrictEqual(properties, { type: 'test', name: 'test' });
       },
     };
     queries.addYAMLMetadata(node, apiEntryMetadata);
@@ -25,7 +25,7 @@ describe('createQueries', () => {
     };
     const parent = { children: [node] };
     queries.updateTypeReference(node, parent);
-    deepStrictEqual(
+    assert.deepStrictEqual(
       parent.children.map(c => c.value),
       [
         'This is a ',
@@ -42,8 +42,8 @@ describe('createQueries', () => {
     };
     const parent = { children: [node] };
     queries.updateTypeReference(node, parent);
-    strictEqual(parent.children[0].type, 'text');
-    strictEqual(parent.children[0].value, 'This is a {test} type.');
+    assert.strictEqual(parent.children[0].type, 'text');
+    assert.strictEqual(parent.children[0].value, 'This is a {test} type.');
   });
 
   it('should add heading metadata correctly', () => {
@@ -53,7 +53,7 @@ describe('createQueries', () => {
     };
     const apiEntryMetadata = {
       setHeading: heading => {
-        deepStrictEqual(heading, {
+        assert.deepStrictEqual(heading, {
           children: [
             {
               type: 'text',
@@ -75,15 +75,15 @@ describe('createQueries', () => {
   it('should update markdown link correctly', () => {
     const node = { type: 'link', url: 'test.md#heading' };
     queries.updateMarkdownLink(node);
-    strictEqual(node.url, 'test.html#heading');
+    assert.strictEqual(node.url, 'test.html#heading');
   });
 
   it('should update link reference correctly', () => {
     const node = { type: 'linkReference', identifier: 'test' };
     const definitions = [{ identifier: 'test', url: 'test.html#test' }];
     queries.updateLinkReference(node, definitions);
-    strictEqual(node.type, 'link');
-    strictEqual(node.url, 'test.html#test');
+    assert.strictEqual(node.type, 'link');
+    assert.strictEqual(node.url, 'test.html#test');
   });
 
   it('should add stability index metadata correctly', () => {
@@ -98,7 +98,7 @@ describe('createQueries', () => {
     };
     const apiEntryMetadata = {
       addStability: stability => {
-        deepStrictEqual(stability.data, {
+        assert.deepStrictEqual(stability.data, {
           index: '1.0',
           description: 'Frozen',
         });
@@ -110,14 +110,14 @@ describe('createQueries', () => {
   describe('UNIST', () => {
     describe('isTypedList', () => {
       it('returns false for non-list nodes', () => {
-        strictEqual(
+        assert.strictEqual(
           createQueries.UNIST.isTypedList({ type: 'paragraph', children: [] }),
           false
         );
       });
 
       it('returns false for empty lists', () => {
-        strictEqual(
+        assert.strictEqual(
           createQueries.UNIST.isTypedList({ type: 'list', children: [] }),
           false
         );
@@ -211,7 +211,7 @@ describe('createQueries', () => {
 
       cases.forEach(({ name, node, expected }) => {
         it(`returns ${expected} for ${name}`, () => {
-          strictEqual(createQueries.UNIST.isTypedList(node), expected);
+          assert.strictEqual(createQueries.UNIST.isTypedList(node), expected);
         });
       });
     });