diff --git a/.changeset/specifier-generator-loading.md b/.changeset/specifier-generator-loading.md new file mode 100644 index 00000000..039de74a --- /dev/null +++ b/.changeset/specifier-generator-loading.md @@ -0,0 +1,11 @@ +--- +'@node-core/doc-kit': minor +--- + +Generators are now loaded dynamically by import specifier instead of a static +registry. `--target` accepts either a built-in shorthand name (`web`, +`legacy-html`, …) or any import specifier resolving to a generator module +(e.g. `some-package/generator` or `./my-generator.mjs`), and a generator's +`dependsOn` is now a full import specifier. This lays the groundwork for +splitting the built-in generators into separate packages and enables +third-party generator packages. diff --git a/docs/configuration.md b/docs/configuration.md index 8364b11d..eef68555 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -36,7 +36,9 @@ uses the `doc-kit` property: ```javascript export default { - // targets, alternatively supplied by command line flags + // Targets, alternatively supplied by command line flags. Each entry is + // either a built-in shorthand name or an import specifier resolving to a + // generator module (e.g. '@my-scope/my-package/my-generator'). target: ['orama-db', 'web'], global: { version: '20.0.0', diff --git a/docs/generators.md b/docs/generators.md index 6134c2a9..33f02c02 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -69,24 +69,26 @@ export type Generator = GeneratorMetadata< ### Step 3: Define Generator Metadata -Create the generator metadata in `index.mjs` using `createLazyGenerator`: +A generator module's default export is a plain object with its metadata and +implementation. Create it in `index.mjs`: ```javascript // packages/core/src/generators/my-format/index.mjs -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; /** * Generates output in MyFormat. * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'my-format', description: 'Generates documentation in MyFormat', - // This generator depends on the metadata generator - dependsOn: 'metadata', + // This generator depends on the metadata generator. Dependencies are + // declared as import specifiers, so they can live in any package. + dependsOn: '@node-core/doc-kit/metadata', defaultConfiguration: { // If your generator supports a custom configuration, define the defaults here @@ -96,7 +98,9 @@ export default createLazyGenerator({ // To override the defaults, they can be specified here ref: 'overriddenRef', }, -}); + + generate, +}; ``` ### Step 4: Implement the Generator Logic @@ -147,28 +151,34 @@ function transformToMyFormat(entries, version) { } ``` -### Step 5: Register the Generator +### Step 5: Make the Generator Loadable -Add your generator to the exports in `packages/core/src/generators/index.mjs`: +Generators are loaded dynamically by import specifier. Anything that resolves +to a module whose default export is a generator works as a `--target`: -```javascript -// For public generators (available via CLI) -import myFormat from './my-format/index.mjs'; +```bash +# A package (subpath) export +doc-kit generate -t @my-scope/my-package/my-format ... + +# A local file +doc-kit generate -t ./generators/my-format/index.mjs ... +``` + +Built-in generators additionally get a shorthand alias in +`packages/core/src/generators/index.mjs`, which maps the name users type to +the import specifier it resolves to: +```javascript export const publicGenerators = { - 'json-simple': jsonSimple, - 'my-format': myFormat, // Add this + 'json-simple': '@node-core/doc-kit/json-simple', + 'my-format': '@node-core/doc-kit/my-format', // Add this // ... other generators }; - -// For internal generators (used only as dependencies) -const internalGenerators = { - ast, - metadata, - // ... internal generators -}; ``` +If the generator lives in this repository, also add a matching subpath to the +`exports` map of its package's `package.json`. + ## Parallel Processing with Workers For generators processing large datasets, implement parallel processing using worker threads. @@ -179,21 +189,24 @@ First, define the generator metadata in `index.mjs`: ```javascript // packages/core/src/generators/parallel-generator/index.mjs -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate, processChunk } from './generate.mjs'; /** * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'parallel-generator', description: 'Processes data in parallel', - dependsOn: 'metadata', + dependsOn: '@node-core/doc-kit/metadata', // Indicates this generator has a processChunk implementation hasParallelProcessor: true, -}); + + generate, + processChunk, +}; ``` Then, implement both `processChunk` and `generate` in `generate.mjs`: @@ -273,20 +286,23 @@ Define the generator metadata in `index.mjs`: ```javascript // packages/core/src/generators/streaming-generator/index.mjs -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate, processChunk } from './generate.mjs'; /** * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'streaming-generator', description: 'Streams results as they are ready', - dependsOn: 'metadata', + dependsOn: '@node-core/doc-kit/metadata', hasParallelProcessor: true, -}); + + generate, + processChunk, +}; ``` Implement the generator in `generate.mjs`: @@ -331,18 +347,20 @@ Generator metadata in `index.mjs`: ```javascript // packages/core/src/generators/batch-generator/index.mjs -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; /** * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'batch-generator', description: 'Requires all input at once', - dependsOn: 'jsx-ast', -}); + dependsOn: '@node-core/doc-kit/jsx-ast', + + generate, +}; ``` Implementation in `generate.mjs`: @@ -378,15 +396,19 @@ Use non-streaming when: In `index.mjs`: ```javascript -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; -export default createLazyGenerator({ +export default { name: 'my-generator', - dependsOn: 'metadata', // This generator requires metadata output + // This generator requires the metadata generator's output. The dependency + // is an import specifier, so it may point at any installed package. + dependsOn: '@node-core/doc-kit/metadata', // ... other metadata -}); + + generate, +}; ``` In `generate.mjs`: @@ -402,27 +424,27 @@ export async function generate(input, worker) { ```javascript // Step 1: Parse markdown to AST // packages/core/src/generators/ast/index.mjs -export default createLazyGenerator({ +export default { name: 'ast', - dependsOn: undefined, // No dependency + dependsOn: undefined, // No dependency // Processes raw markdown files -}); +}; // Step 2: Extract metadata from AST // packages/core/src/generators/metadata/index.mjs -export default createLazyGenerator({ +export default { name: 'metadata', - dependsOn: 'ast', // Depends on AST + dependsOn: '@node-core/doc-kit/ast', // Depends on AST // Processes AST output -}); +}; // Step 3: Generate HTML from metadata // packages/core/src/generators/html-generator/index.mjs -export default createLazyGenerator({ +export default { name: 'html-generator', - dependsOn: 'metadata', // Depends on metadata + dependsOn: '@node-core/doc-kit/metadata', // Depends on metadata // Processes metadata output -}); +}; ``` ### Multiple Consumers diff --git a/packages/core/bin/commands/generate.mjs b/packages/core/bin/commands/generate.mjs index 60947204..90e8c9d3 100644 --- a/packages/core/bin/commands/generate.mjs +++ b/packages/core/bin/commands/generate.mjs @@ -36,8 +36,11 @@ export default new Command('generate') new Option('-i, --input ', 'Input file patterns (glob)') ) .addOption( - new Option('-t, --target ', 'Target generator(s)').choices( - Object.keys(publicGenerators) + new Option( + '-t, --target ', + 'Target generator(s): a built-in name ' + + `(${Object.keys(publicGenerators).join(', ')}) ` + + 'or an import specifier for a custom generator' ) ) .addOption( diff --git a/packages/core/package.json b/packages/core/package.json index 877ca3e5..b5f668a7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -16,6 +16,29 @@ "watch": "node --watch bin/cli.mjs" }, "main": "./src/generators.mjs", + "exports": { + ".": "./src/generators.mjs", + "./addon-verify": "./src/generators/addon-verify/index.mjs", + "./api-links": "./src/generators/api-links/index.mjs", + "./ast": "./src/generators/ast/index.mjs", + "./ast-js": "./src/generators/ast-js/index.mjs", + "./json-simple": "./src/generators/json-simple/index.mjs", + "./jsx-ast": "./src/generators/jsx-ast/index.mjs", + "./legacy-html": "./src/generators/legacy-html/index.mjs", + "./legacy-html-all": "./src/generators/legacy-html-all/index.mjs", + "./legacy-json": "./src/generators/legacy-json/index.mjs", + "./legacy-json-all": "./src/generators/legacy-json-all/index.mjs", + "./llms-txt": "./src/generators/llms-txt/index.mjs", + "./man-page": "./src/generators/man-page/index.mjs", + "./metadata": "./src/generators/metadata/index.mjs", + "./orama-db": "./src/generators/orama-db/index.mjs", + "./sitemap": "./src/generators/sitemap/index.mjs", + "./web": "./src/generators/web/index.mjs", + "./package.json": "./package.json", + "./shiki.config.mjs": "./shiki.config.mjs", + "./src/*": "./src/*", + "./*": "./src/*" + }, "bin": { "doc-kit": "./bin/cli.mjs" }, diff --git a/packages/core/src/__tests__/generators.test.mjs b/packages/core/src/__tests__/generators.test.mjs index bf503b4f..40af019f 100644 --- a/packages/core/src/__tests__/generators.test.mjs +++ b/packages/core/src/__tests__/generators.test.mjs @@ -16,63 +16,88 @@ const streamOf = chunk => yield chunk; })(); -mock.module('../generators/index.mjs', { +// Synthetic generators keyed by specifier (any string works as a specifier) +const syntheticGenerators = { + // Root streaming generator (no dependency) + ast: { + name: 'ast', + hasParallelProcessor: true, + generate: async () => { + record('ast'); + return streamOf([{ ast: true }]); + }, + }, + // Streaming generator shared by multiple consumers + metadata: { + name: 'metadata', + dependsOn: 'ast', + hasParallelProcessor: true, + generate: async input => { + record('metadata'); + return streamOf([{ meta: input.length }]); + }, + }, + // Two non-streaming consumers of the shared `metadata` result + 'gen-a': { + name: 'gen-a', + dependsOn: 'metadata', + generate: async input => { + record('gen-a'); + return { a: input }; + }, + }, + 'gen-b': { + name: 'gen-b', + dependsOn: 'metadata', + generate: async input => { + record('gen-b'); + return { b: input }; + }, + }, + // A target that is itself depended upon by another target + 'gen-c': { + name: 'gen-c', + dependsOn: 'metadata', + hasParallelProcessor: true, + generate: async () => { + record('gen-c'); + return streamOf([{ c: true }]); + }, + }, + 'gen-c-all': { + name: 'gen-c-all', + dependsOn: 'gen-c', + generate: async input => { + record('gen-c-all'); + return { all: input }; + }, + }, +}; + +mock.module('../generators/loader.mjs', { namedExports: { - allGenerators: { - // Root streaming generator (no dependency) - ast: { - name: 'ast', - hasParallelProcessor: true, - generate: async () => { - record('ast'); - return streamOf([{ ast: true }]); - }, - }, - // Streaming generator shared by multiple consumers - metadata: { - name: 'metadata', - dependsOn: 'ast', - hasParallelProcessor: true, - generate: async input => { - record('metadata'); - return streamOf([{ meta: input.length }]); - }, - }, - // Two non-streaming consumers of the shared `metadata` result - 'gen-a': { - name: 'gen-a', - dependsOn: 'metadata', - generate: async input => { - record('gen-a'); - return { a: input }; - }, - }, - 'gen-b': { - name: 'gen-b', - dependsOn: 'metadata', - generate: async input => { - record('gen-b'); - return { b: input }; - }, - }, - // A target that is itself depended upon by another target - 'gen-c': { - name: 'gen-c', - dependsOn: 'metadata', - hasParallelProcessor: true, - generate: async () => { - record('gen-c'); - return streamOf([{ c: true }]); - }, - }, - 'gen-c-all': { - name: 'gen-c-all', - dependsOn: 'gen-c', - generate: async input => { - record('gen-c-all'); - return { all: input }; - }, - }, + resolveGeneratorSpecifier: specifier => specifier, + loadGenerator: async specifier => syntheticGenerators[specifier], + loadGenerators: async targets => { + const generators = new Map(); + const queue = [...targets]; + + while (queue.length > 0) { + const specifier = queue.shift(); + + if (generators.has(specifier)) { + continue; + } + + const generator = syntheticGenerators[specifier]; + generators.set(specifier, generator); + + if (generator.dependsOn) { + queue.push(generator.dependsOn); + } + } + + return generators; }, }, }); diff --git a/packages/core/src/caching.mjs b/packages/core/src/caching.mjs index 47dc47da..9ad72110 100644 --- a/packages/core/src/caching.mjs +++ b/packages/core/src/caching.mjs @@ -69,7 +69,7 @@ export const createCache = () => { * Collects an async generator's chunks, ensuring a single collection is * shared across all consumers of the same key. * - * @param {string} key - Cache key (the generator name) + * @param {string} key - Cache key (resolved generator specifier) * @param {AsyncGenerator} generator - Generator to collect * @returns {Promise} Promise resolving to the collected items */ @@ -93,7 +93,7 @@ export const createCache = () => { /** * Whether a generator has already been scheduled. * - * @param {string} name - Generator name + * @param {string} name - Cache key (resolved generator specifier) * @returns {boolean} */ has: name => name in results, @@ -101,7 +101,7 @@ export const createCache = () => { /** * Registers a generator's pending result. * - * @param {string} name - Generator name + * @param {string} name - Cache key (resolved generator specifier) * @param {Promise | AsyncGenerator} result - Pending result */ store: (name, result) => { @@ -115,7 +115,7 @@ export const createCache = () => { * final collection). This drives eviction without coupling the cache to the * generator registry: callers supply how to resolve a dependency. * - * @param {string[]} targets - Requested generator names + * @param {string[]} targets - Requested generator cache keys * @param {(name: string) => string | undefined} dependsOn - Resolves a * generator's dependency name (if any) */ @@ -159,7 +159,7 @@ export const createCache = () => { * Reads a generator's result (collecting it first if it streams) and * evicts it once every expected consumer has read it. * - * @param {string} name - Generator name to consume + * @param {string} name - Cache key (resolved generator specifier) to consume * @returns {Promise} */ consume: async name => { diff --git a/packages/core/src/generators.mjs b/packages/core/src/generators.mjs index 4f87ec29..b079dc0d 100644 --- a/packages/core/src/generators.mjs +++ b/packages/core/src/generators.mjs @@ -1,7 +1,10 @@ 'use strict'; import { createCache } from './caching.mjs'; -import { allGenerators } from './generators/index.mjs'; +import { + loadGenerators, + resolveGeneratorSpecifier, +} from './generators/loader.mjs'; import logger from './logger/index.mjs'; import createWorkerPool from './threading/index.mjs'; import createParallelWorker from './threading/parallel.mjs'; @@ -25,7 +28,7 @@ const createGenerator = () => { /** * Gets the collected input from a dependency generator. * - * @param {string | undefined} dependsOn - Dependency generator name + * @param {string | undefined} dependsOn - Resolved dependency specifier * @returns {Promise} */ const getDependencyInput = async dependsOn => { @@ -39,38 +42,42 @@ const createGenerator = () => { /** * Schedules a generator and its dependencies for execution. * - * @param {string} generatorName - Generator to schedule + * @param {string} specifier - Resolved generator specifier to schedule + * @param {Map} generators - Loaded generators * @param {import('./utils/configuration/types').Configuration} configuration - Runtime options */ - const scheduleGenerator = async (generatorName, configuration) => { - if (cache.has(generatorName)) { + const scheduleGenerator = (specifier, generators, configuration) => { + if (cache.has(specifier)) { return; } - const { dependsOn, generate, hasParallelProcessor } = - allGenerators[generatorName]; + const generator = generators.get(specifier); + const { name, generate, hasParallelProcessor } = generator; + + const dependsOn = + generator.dependsOn && resolveGeneratorSpecifier(generator.dependsOn); // Schedule dependency first if (dependsOn && !cache.has(dependsOn)) { - await scheduleGenerator(dependsOn, configuration); + scheduleGenerator(dependsOn, generators, configuration); } - generatorsLogger.debug(`Scheduling "${generatorName}"`, { + generatorsLogger.debug(`Scheduling "${name}"`, { dependsOn: dependsOn || 'none', streaming: hasParallelProcessor, }); // Schedule the generator cache.store( - generatorName, + specifier, (async () => { const dependencyInput = await getDependencyInput(dependsOn); - generatorsLogger.debug(`Starting "${generatorName}"`); + generatorsLogger.debug(`Starting "${name}"`); // Create parallel worker for streaming generators const worker = hasParallelProcessor - ? createParallelWorker(generatorName, pool, configuration) + ? createParallelWorker(specifier, generator, pool, configuration) : Promise.resolve(null); const result = await generate(dependencyInput, await worker); @@ -78,7 +85,7 @@ const createGenerator = () => { // For streaming generators, "Completed" is logged when the cache // finishes collecting, not here when the generator returns if (!isAsyncIterable(result)) { - generatorsLogger.debug(`Completed "${generatorName}"`); + generatorsLogger.debug(`Completed "${name}"`); } return result; @@ -89,36 +96,42 @@ const createGenerator = () => { /** * Runs all requested generators with their dependencies. * - * @param {import('./utils/configuration/types').Configuration} options - Runtime options + * @param {import('./utils/configuration/types').Configuration} configuration - Runtime options * @returns {Promise} Results of all requested generators */ const runGenerators = async configuration => { - const { target: generators, threads } = configuration; + const { target, threads } = configuration; + + // Resolve shorthand names and load the full dependency closure up front, + // so scheduling below is fully synchronous. + const targets = target.map(resolveGeneratorSpecifier); + const generators = await loadGenerators(targets); generatorsLogger.debug(`Starting pipeline`, { - generators: generators.join(', '), + generators: targets.join(', '), threads, }); // Compute consumer counts up front so dependencies can be evicted as soon // as their last consumer runs (must be ready before any generator starts). - cache.populateConsumerCounts( - generators, - name => allGenerators[name].dependsOn - ); + cache.populateConsumerCounts(targets, specifier => { + const { dependsOn } = generators.get(specifier); + + return dependsOn && resolveGeneratorSpecifier(dependsOn); + }); // Create worker pool pool = createWorkerPool(threads); // Schedule all generators - for (const name of generators) { - await scheduleGenerator(name, configuration); + for (const specifier of targets) { + scheduleGenerator(specifier, generators, configuration); } // Start all collections in parallel (don't await sequentially). Consuming // through the shared path lets the final read also trigger eviction. const results = await Promise.all( - generators.map(name => cache.consume(name)) + targets.map(specifier => cache.consume(specifier)) ); await pool.destroy(); diff --git a/packages/core/src/generators/__tests__/index.test.mjs b/packages/core/src/generators/__tests__/index.test.mjs index 865eac65..cd3bfdcf 100644 --- a/packages/core/src/generators/__tests__/index.test.mjs +++ b/packages/core/src/generators/__tests__/index.test.mjs @@ -1,36 +1,57 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { allGenerators } from '../index.mjs'; +import { allGenerators, publicGenerators } from '../index.mjs'; +import { loadGenerator, resolveGeneratorSpecifier } from '../loader.mjs'; -const validDependencies = Object.keys(allGenerators); +const validDependencies = Object.values(allGenerators); -const allGeneratorsEntries = Object.entries(allGenerators); +const loadedGenerators = await Promise.all( + Object.entries(allGenerators).map(async ([name, specifier]) => [ + name, + specifier, + await loadGenerator(specifier), + ]) +); describe('All Generators', () => { - it('should have keys matching their name property', () => { - allGeneratorsEntries.forEach(([key, generator]) => { + it('should expose public generators as a subset of all generators', () => { + Object.entries(publicGenerators).forEach(([name, specifier]) => { + assert.equal(allGenerators[name], specifier); + }); + }); + + it('should resolve shorthand names to their specifiers', () => { + Object.entries(allGenerators).forEach(([name, specifier]) => { + assert.equal(resolveGeneratorSpecifier(name), specifier); + // Resolution must be idempotent for already-resolved specifiers + assert.equal(resolveGeneratorSpecifier(specifier), specifier); + }); + }); + + it('should have shorthand names matching their name property', () => { + loadedGenerators.forEach(([name, , generator]) => { assert.equal( - key, + name, generator.name, - `Generator key "${key}" does not match its name property "${generator.name}"` + `Alias "${name}" does not match its name property "${generator.name}"` ); }); }); it('should have valid dependsOn references', () => { - allGeneratorsEntries.forEach(([key, generator]) => { + loadedGenerators.forEach(([name, , generator]) => { if (generator.dependsOn) { assert.ok( validDependencies.includes(generator.dependsOn), - `Generator "${key}" depends on "${generator.dependsOn}" which is not a valid generator` + `Generator "${name}" depends on "${generator.dependsOn}" which is not a valid generator specifier` ); } }); }); - it('should have ast generator as a top-level generator with no dependencies', () => { - const ast = allGenerators.ast; + it('should have ast generator as a top-level generator with no dependencies', async () => { + const ast = await loadGenerator(allGenerators.ast); assert.ok(ast, 'ast generator should exist'); assert.equal( ast.dependsOn, diff --git a/packages/core/src/generators/addon-verify/index.mjs b/packages/core/src/generators/addon-verify/index.mjs index 8f6c4c70..7b5515d1 100644 --- a/packages/core/src/generators/addon-verify/index.mjs +++ b/packages/core/src/generators/addon-verify/index.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; /** * This generator generates a file list from code blocks extracted from @@ -9,11 +9,13 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default await createLazyGenerator({ +export default { name: 'addon-verify', description: 'Generates a file list from code blocks extracted from `doc/api/addons.md` to facilitate C++ compilation and JavaScript runtime validations', - dependsOn: 'metadata', -}); + dependsOn: '@node-core/doc-kit/metadata', + + generate, +}; diff --git a/packages/core/src/generators/api-links/__tests__/fixtures.test.mjs b/packages/core/src/generators/api-links/__tests__/fixtures.test.mjs index 6259aeb3..f3199e6d 100644 --- a/packages/core/src/generators/api-links/__tests__/fixtures.test.mjs +++ b/packages/core/src/generators/api-links/__tests__/fixtures.test.mjs @@ -3,6 +3,7 @@ import { after, before, describe, it } from 'node:test'; import { globSync } from 'tinyglobby'; +import { loadGenerator } from '../../../generators/loader.mjs'; import createWorkerPool from '../../../threading/index.mjs'; import createParallelWorker from '../../../threading/parallel.mjs'; import { setConfig } from '../../../utils/configuration/index.mjs'; @@ -15,7 +16,7 @@ const sourceFiles = globSync('*.js', { cwd: new URL(import.meta.resolve('./fixtures')), }); -const config = await setConfig({}); +const config = await setConfig({ target: ['api-links'] }); describe('api links', () => { let pool; @@ -35,7 +36,12 @@ describe('api links', () => { join(relativePath, 'fixtures', sourceFile).replaceAll(sep, '/'), ]; - const worker = await createParallelWorker('ast-js', pool, config); + const worker = createParallelWorker( + 'ast-js', + await loadGenerator('ast-js'), + pool, + config + ); // Collect results from the async generator const astJsResults = []; diff --git a/packages/core/src/generators/api-links/index.mjs b/packages/core/src/generators/api-links/index.mjs index 58b69fe3..fb988f99 100644 --- a/packages/core/src/generators/api-links/index.mjs +++ b/packages/core/src/generators/api-links/index.mjs @@ -1,7 +1,7 @@ 'use strict'; +import { generate } from './generate.mjs'; import { GITHUB_BLOB_URL } from '../../utils/configuration/templates.mjs'; -import { createLazyGenerator } from '../../utils/generators.mjs'; /** * This generator is responsible for mapping publicly accessible functions in @@ -13,7 +13,7 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'api-links', description: @@ -21,9 +21,11 @@ export default createLazyGenerator({ // Unlike the rest of the generators, this utilizes Javascript sources being // passed into the input field rather than Markdown. - dependsOn: 'ast-js', + dependsOn: '@node-core/doc-kit/ast-js', defaultConfiguration: { sourceURL: `${GITHUB_BLOB_URL}lib/{fileName}`, }, -}); + + generate, +}; diff --git a/packages/core/src/generators/ast-js/index.mjs b/packages/core/src/generators/ast-js/index.mjs index 79e3f564..2b169e54 100644 --- a/packages/core/src/generators/ast-js/index.mjs +++ b/packages/core/src/generators/ast-js/index.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate, processChunk } from './generate.mjs'; /** * This generator parses Javascript sources passed into the generator's input @@ -12,10 +12,13 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'ast-js', description: 'Parses Javascript source files passed into the input.', hasParallelProcessor: true, -}); + + generate, + processChunk, +}; diff --git a/packages/core/src/generators/ast/index.mjs b/packages/core/src/generators/ast/index.mjs index 52297f5e..b9c51974 100644 --- a/packages/core/src/generators/ast/index.mjs +++ b/packages/core/src/generators/ast/index.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate, processChunk } from './generate.mjs'; /** * This generator parses Markdown API doc files into AST trees. @@ -8,10 +8,13 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'ast', description: 'Parses Markdown API doc files into AST trees', hasParallelProcessor: true, -}); + + generate, + processChunk, +}; diff --git a/packages/core/src/generators/index.mjs b/packages/core/src/generators/index.mjs index 24294f92..a8b1a68f 100644 --- a/packages/core/src/generators/index.mjs +++ b/packages/core/src/generators/index.mjs @@ -1,44 +1,37 @@ 'use strict'; -import addonVerify from './addon-verify/index.mjs'; -import apiLinks from './api-links/index.mjs'; -import ast from './ast/index.mjs'; -import astJs from './ast-js/index.mjs'; -import jsonSimple from './json-simple/index.mjs'; -import jsxAst from './jsx-ast/index.mjs'; -import legacyHtml from './legacy-html/index.mjs'; -import legacyHtmlAll from './legacy-html-all/index.mjs'; -import legacyJson from './legacy-json/index.mjs'; -import legacyJsonAll from './legacy-json-all/index.mjs'; -import llmsTxt from './llms-txt/index.mjs'; -import manPage from './man-page/index.mjs'; -import metadata from './metadata/index.mjs'; -import oramaDb from './orama-db/index.mjs'; -import sitemap from './sitemap/index.mjs'; -import web from './web/index.mjs'; - +/** + * Maps the shorthand names accepted by the CLI and configuration files + * (e.g. `--target web`) to the import specifiers they resolve to. + * + * Generators are loaded dynamically by specifier (see `./loader.mjs`), so this + * module must not import any generator code — it is purely a lookup table. + * Anything that is not listed here is treated as a raw import specifier, + * which is how third-party generator packages are loaded. + */ export const publicGenerators = { - 'json-simple': jsonSimple, - 'legacy-html': legacyHtml, - 'legacy-html-all': legacyHtmlAll, - 'man-page': manPage, - 'legacy-json': legacyJson, - 'legacy-json-all': legacyJsonAll, - 'addon-verify': addonVerify, - 'api-links': apiLinks, - 'orama-db': oramaDb, - 'llms-txt': llmsTxt, - sitemap, - web, + 'json-simple': '@node-core/doc-kit/json-simple', + 'legacy-html': '@node-core/doc-kit/legacy-html', + 'legacy-html-all': '@node-core/doc-kit/legacy-html-all', + 'man-page': '@node-core/doc-kit/man-page', + 'legacy-json': '@node-core/doc-kit/legacy-json', + 'legacy-json-all': '@node-core/doc-kit/legacy-json-all', + 'addon-verify': '@node-core/doc-kit/addon-verify', + 'api-links': '@node-core/doc-kit/api-links', + 'orama-db': '@node-core/doc-kit/orama-db', + 'llms-txt': '@node-core/doc-kit/llms-txt', + sitemap: '@node-core/doc-kit/sitemap', + web: '@node-core/doc-kit/web', }; // These ones are special since they don't produce standard output, -// and hence, we don't expose them to the CLI. +// and hence, we don't expose them to the CLI. They can still be referenced +// by shorthand (e.g. as configuration keys or generator dependencies). const internalGenerators = { - ast, - metadata, - 'jsx-ast': jsxAst, - 'ast-js': astJs, + ast: '@node-core/doc-kit/ast', + metadata: '@node-core/doc-kit/metadata', + 'jsx-ast': '@node-core/doc-kit/jsx-ast', + 'ast-js': '@node-core/doc-kit/ast-js', }; export const allGenerators = { diff --git a/packages/core/src/generators/json-simple/index.mjs b/packages/core/src/generators/json-simple/index.mjs index 04bf2bee..5ddef295 100644 --- a/packages/core/src/generators/json-simple/index.mjs +++ b/packages/core/src/generators/json-simple/index.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; /** * This generator generates a simplified JSON version of the API docs and returns it as a string @@ -11,11 +11,13 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'json-simple', description: 'Generates the simple JSON version of the API docs, and returns it as a string', - dependsOn: 'metadata', -}); + dependsOn: '@node-core/doc-kit/metadata', + + generate, +}; diff --git a/packages/core/src/generators/jsx-ast/__tests__/generate.test.mjs b/packages/core/src/generators/jsx-ast/__tests__/generate.test.mjs index c1822024..e1f52e76 100644 --- a/packages/core/src/generators/jsx-ast/__tests__/generate.test.mjs +++ b/packages/core/src/generators/jsx-ast/__tests__/generate.test.mjs @@ -58,7 +58,7 @@ const createWorker = seenItems => ({ describe('jsx-ast generate', () => { it('does not attach raw section entries to regular JSX content', async () => { - await setConfig({}); + await setConfig({ target: ['jsx-ast'] }); const fs = createEntry('fs', 'File system'); const [content] = await processChunk([{ head: fs, entries: [fs] }], [0]); @@ -68,7 +68,7 @@ describe('jsx-ast generate', () => { }); it('respects jsx-ast synthetic page flags', async () => { - await setConfig({}); + await setConfig({ target: ['jsx-ast'] }); const jsxAstConfig = getConfig('jsx-ast'); jsxAstConfig.generateAllPage = false; diff --git a/packages/core/src/generators/jsx-ast/index.mjs b/packages/core/src/generators/jsx-ast/index.mjs index f1a982d6..2060ed9f 100644 --- a/packages/core/src/generators/jsx-ast/index.mjs +++ b/packages/core/src/generators/jsx-ast/index.mjs @@ -1,18 +1,18 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate, processChunk } from './generate.mjs'; /** * Generator for converting MDAST to JSX AST. * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'jsx-ast', description: 'Generates JSX AST from the input MDAST', - dependsOn: 'metadata', + dependsOn: '@node-core/doc-kit/metadata', defaultConfiguration: { ref: 'main', @@ -22,4 +22,7 @@ export default createLazyGenerator({ }, hasParallelProcessor: true, -}); + + generate, + processChunk, +}; diff --git a/packages/core/src/generators/jsx-ast/utils/signature.mjs b/packages/core/src/generators/jsx-ast/utils/signature.mjs index 42d4cf4b..f7220d02 100644 --- a/packages/core/src/generators/jsx-ast/utils/signature.mjs +++ b/packages/core/src/generators/jsx-ast/utils/signature.mjs @@ -4,15 +4,15 @@ import { createJSXElement } from './ast.mjs'; import { parseListIntoProperties } from './types.mjs'; import { highlighter } from '../../../utils/highlighter.mjs'; import { UNIST } from '../../../utils/queries/index.mjs'; -import { parseListItem } from '../../legacy-json/utils/parseList.mjs'; -import parseSignature from '../../legacy-json/utils/parseSignature.mjs'; +import { parseListItem } from '../../../utils/signature/parseList.mjs'; +import parseSignature from '../../../utils/signature/parseSignature.mjs'; import { JSX_IMPORTS } from '../../web/constants.mjs'; /** * Generates a string representation of a function or class signature. * * @param {string} functionName - The name of the function or class. - * @param {import('../../legacy-json/types').MethodSignature} signature - The parsed signature object. + * @param {import('../../../utils/signature/types').MethodSignature} signature - The parsed signature object. * @param {string} prefix - Optional prefix, i.e. `'new '` for constructors. */ export const generateSignature = ( @@ -52,7 +52,7 @@ export const generateSignature = ( * Creates a syntax-highlighted code block for a signature using rehype-shiki. * * @param {string} functionName - The function name to display. - * @param {import('../../legacy-json/types').MethodSignature} signature - Signature object with parameter and return type info. + * @param {import('../../../utils/signature/types').MethodSignature} signature - Signature object with parameter and return type info. * @param {string} prefix - Optional prefix like `'new '`. */ export const createSignatureCodeBlock = (functionName, signature, prefix) => { diff --git a/packages/core/src/generators/jsx-ast/utils/types.mjs b/packages/core/src/generators/jsx-ast/utils/types.mjs index aab6b70f..706f7dd9 100644 --- a/packages/core/src/generators/jsx-ast/utils/types.mjs +++ b/packages/core/src/generators/jsx-ast/utils/types.mjs @@ -2,8 +2,8 @@ import { u as createTree } from 'unist-builder'; import { QUERIES, UNIST } from '../../../utils/queries/index.mjs'; import { getRemarkRecma as remark } from '../../../utils/remark.mjs'; +import { DEFAULT_EXPRESSION } from '../../../utils/signature/constants.mjs'; import { transformNodesToString } from '../../../utils/unist.mjs'; -import { DEFAULT_EXPRESSION } from '../../legacy-json/constants.mjs'; import { TRIMMABLE_PADDING_REGEX } from '../constants.mjs'; /** diff --git a/packages/core/src/generators/legacy-html-all/index.mjs b/packages/core/src/generators/legacy-html-all/index.mjs index 088a7076..4d6f1d35 100644 --- a/packages/core/src/generators/legacy-html-all/index.mjs +++ b/packages/core/src/generators/legacy-html-all/index.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; import legacyHtml from '../legacy-html/index.mjs'; /** @@ -12,15 +12,17 @@ import legacyHtml from '../legacy-html/index.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'legacy-html-all', description: 'Generates the `all.html` file from the `legacy-html` generator, which includes all the modules in one single file', - dependsOn: 'legacy-html', + dependsOn: '@node-core/doc-kit/legacy-html', defaultConfiguration: { templatePath: legacyHtml.defaultConfiguration.templatePath, }, -}); + + generate, +}; diff --git a/packages/core/src/generators/legacy-html/index.mjs b/packages/core/src/generators/legacy-html/index.mjs index caf12ee3..68c4ea9f 100644 --- a/packages/core/src/generators/legacy-html/index.mjs +++ b/packages/core/src/generators/legacy-html/index.mjs @@ -2,8 +2,8 @@ import { join } from 'node:path'; +import { generate, processChunk } from './generate.mjs'; import { GITHUB_EDIT_URL } from '../../utils/configuration/templates.mjs'; -import { createLazyGenerator } from '../../utils/generators.mjs'; /** * @@ -15,13 +15,13 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'legacy-html', description: 'Generates the legacy version of the API docs in HTML, with the assets and styles included as files', - dependsOn: 'metadata', + dependsOn: '@node-core/doc-kit/metadata', defaultConfiguration: { templatePath: join(import.meta.dirname, 'template.html'), @@ -32,4 +32,7 @@ export default createLazyGenerator({ }, hasParallelProcessor: true, -}); + + generate, + processChunk, +}; diff --git a/packages/core/src/generators/legacy-json-all/index.mjs b/packages/core/src/generators/legacy-json-all/index.mjs index f58812e1..38af8498 100644 --- a/packages/core/src/generators/legacy-json-all/index.mjs +++ b/packages/core/src/generators/legacy-json-all/index.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; /** * This generator consolidates data from the `legacy-json` generator into a single @@ -8,15 +8,17 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types.d.ts').Generator} */ -export default createLazyGenerator({ +export default { name: 'legacy-json-all', description: 'Generates the `all.json` file from the `legacy-json` generator, which includes all the modules in one single file.', - dependsOn: 'legacy-json', + dependsOn: '@node-core/doc-kit/legacy-json', defaultConfiguration: { minify: false, }, -}); + + generate, +}; diff --git a/packages/core/src/generators/legacy-json/constants.mjs b/packages/core/src/generators/legacy-json/constants.mjs index 4ae94f6f..b4ecf48f 100644 --- a/packages/core/src/generators/legacy-json/constants.mjs +++ b/packages/core/src/generators/legacy-json/constants.mjs @@ -1,16 +1,3 @@ -// Grabs a method's name -export const NAME_EXPRESSION = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/; - -// Checks if there's a leading hyphen -export const LEADING_HYPHEN = /^-\s*/; - -// Grabs the default value if present -export const DEFAULT_EXPRESSION = /\s*\*\*Default:\*\*\s*([^]+)$/i; - -// Grabs the parameters from a method's signature -// ex/ 'new buffer.Blob([sources[, options]])'.match(PARAM_EXPRESSION) === ['([sources[, options]])', '[sources[, options]]'] -export const PARAM_EXPRESSION = /\(([^)]+)\);?$/; - // The plurals associated with each section type. export const SECTION_TYPE_PLURALS = { module: 'modules', diff --git a/packages/core/src/generators/legacy-json/index.mjs b/packages/core/src/generators/legacy-json/index.mjs index e3cbab1e..fdb71717 100644 --- a/packages/core/src/generators/legacy-json/index.mjs +++ b/packages/core/src/generators/legacy-json/index.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate, processChunk } from './generate.mjs'; /** * This generator is responsible for generating the legacy JSON files for the @@ -13,12 +13,12 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'legacy-json', description: 'Generates the legacy version of the JSON API docs.', - dependsOn: 'metadata', + dependsOn: '@node-core/doc-kit/metadata', defaultConfiguration: { ref: 'main', @@ -26,4 +26,7 @@ export default createLazyGenerator({ }, hasParallelProcessor: true, -}); + + generate, + processChunk, +}; diff --git a/packages/core/src/generators/legacy-json/types.d.ts b/packages/core/src/generators/legacy-json/types.d.ts index 2d9a664d..058ffc65 100644 --- a/packages/core/src/generators/legacy-json/types.d.ts +++ b/packages/core/src/generators/legacy-json/types.d.ts @@ -1,5 +1,6 @@ import { ListItem } from '@types/mdast'; import { MetadataEntry } from '../metadata/types'; +import { MethodSignature } from '../../utils/signature/types'; /** * A node in the entry hierarchy. @@ -153,41 +154,6 @@ export type Section = | EventSection | MiscSection; -/** - * Represents a parameter for methods or functions. - */ -export interface Parameter { - /** - * The name of the parameter. - */ - name: string; - - /** - * Indicates if the parameter is optional. - */ - optional?: boolean; - - /** - * The default value for the parameter. - */ - default?: string; -} - -/** - * Represents a method signature, including its parameters and return type. - */ -export interface MethodSignature { - /** - * A list of parameters for the method. - */ - params: Parameter[]; - - /** - * The return type of the method. - */ - return?: Parameter; -} - /** * Represents a property section in the API documentation. */ @@ -230,38 +196,6 @@ export interface MiscSection extends SectionBase { [key: string]: string | undefined; } -/** - * Represents a list of parameters. - */ -export interface ParameterList { - /** - * Raw parameter description - */ - textRaw: string; - - /** - * A short description of the parameter. - */ - desc?: string; - - /** - * The name of the parameter. - */ - name: string; - - /** - * The type of the parameter (E.G. string, boolean). - */ - type?: string; - - /** - * The default value. - */ - default?: string; - - options?: ParameterList; -} - export type Generator = GeneratorMetadata< {}, Generate, AsyncGenerator
>, diff --git a/packages/core/src/generators/legacy-json/utils/buildSection.mjs b/packages/core/src/generators/legacy-json/utils/buildSection.mjs index c891cf16..41b2d222 100644 --- a/packages/core/src/generators/legacy-json/utils/buildSection.mjs +++ b/packages/core/src/generators/legacy-json/utils/buildSection.mjs @@ -1,7 +1,7 @@ import { buildHierarchy } from './buildHierarchy.mjs'; -import { parseList } from './parseList.mjs'; import { enforceArray } from '../../../utils/array.mjs'; import { getRemarkRehype as remark } from '../../../utils/remark.mjs'; +import { parseList } from '../../../utils/signature/parseList.mjs'; import { transformNodesToString } from '../../../utils/unist.mjs'; import { SECTION_TYPE_PLURALS, UNPROMOTED_KEYS } from '../constants.mjs'; diff --git a/packages/core/src/generators/llms-txt/index.mjs b/packages/core/src/generators/llms-txt/index.mjs index 8c244128..9547625d 100644 --- a/packages/core/src/generators/llms-txt/index.mjs +++ b/packages/core/src/generators/llms-txt/index.mjs @@ -2,7 +2,7 @@ import { join } from 'node:path'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; /** * This generator generates a llms.txt file to provide information to LLMs at @@ -10,16 +10,18 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'llms-txt', description: 'Generates a llms.txt file to provide information to LLMs at inference time', - dependsOn: 'metadata', + dependsOn: '@node-core/doc-kit/metadata', defaultConfiguration: { templatePath: join(import.meta.dirname, 'template.txt'), pageURL: '{baseURL}/latest/api{path}.md', }, -}); + + generate, +}; diff --git a/packages/core/src/generators/loader.mjs b/packages/core/src/generators/loader.mjs new file mode 100644 index 00000000..653d202b --- /dev/null +++ b/packages/core/src/generators/loader.mjs @@ -0,0 +1,96 @@ +'use strict'; + +import { isAbsolute } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { allGenerators } from './index.mjs'; + +/** + * Resolves a CLI/configuration target into an import specifier. Shorthand + * names map through the alias table; filesystem paths become file URLs; + * anything else is treated as an import specifier already. + * + * Resolution is idempotent, so already-resolved specifiers pass through. + * + * @param {string} target - Shorthand name, path, or import specifier + * @returns {string} The import specifier to load the generator from + */ +export const resolveGeneratorSpecifier = target => { + if (target in allGenerators) { + return allGenerators[target]; + } + + if (target.startsWith('.') || isAbsolute(target)) { + return pathToFileURL(target).href; + } + + return target; +}; + +/** + * Imports a generator by specifier and returns its default export. + * + * @param {string} specifier - Shorthand name, path, or import specifier + * @returns {Promise} + */ +export const loadGenerator = async specifier => { + const resolved = resolveGeneratorSpecifier(specifier); + + /** @type {{ default?: GeneratorMetadata }} */ + let module; + + try { + module = await import(resolved); + } catch (error) { + if (error.code === 'ERR_MODULE_NOT_FOUND') { + throw new Error( + `Could not load generator "${specifier}" (resolved to "${resolved}"). ` + + 'If it lives in a separate package, make sure that package is installed.', + { cause: error } + ); + } + + throw error; + } + + const generator = module.default; + + if (!generator?.name || typeof generator.generate !== 'function') { + throw new Error( + `"${specifier}" is not a generator: expected a default export ` + + 'with a `name` and a `generate` function.' + ); + } + + return generator; +}; + +/** + * Loads the given generators plus the transitive closure of their + * dependencies (via `dependsOn`). + * + * @param {string[]} targets - Shorthand names, paths, or import specifiers + * @returns {Promise>} Loaded generators keyed + * by their resolved specifier + */ +export const loadGenerators = async targets => { + const generators = new Map(); + const queue = targets.map(resolveGeneratorSpecifier); + + while (queue.length > 0) { + const specifier = queue.shift(); + + if (generators.has(specifier)) { + continue; + } + + const generator = await loadGenerator(specifier); + generators.set(specifier, generator); + + if (generator.dependsOn) { + queue.push(resolveGeneratorSpecifier(generator.dependsOn)); + } + } + + return generators; +}; diff --git a/packages/core/src/generators/man-page/index.mjs b/packages/core/src/generators/man-page/index.mjs index 92e1c41d..6b3d035c 100644 --- a/packages/core/src/generators/man-page/index.mjs +++ b/packages/core/src/generators/man-page/index.mjs @@ -2,7 +2,7 @@ import { join } from 'node:path'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; /** * This generator generates a man page version of the CLI.md file. @@ -10,12 +10,12 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'man-page', description: 'Generates the Node.js man-page.', - dependsOn: 'metadata', + dependsOn: '@node-core/doc-kit/metadata', defaultConfiguration: { fileName: 'node.1', @@ -23,4 +23,6 @@ export default createLazyGenerator({ envVarsHeaderSlug: 'environment-variables-1', templatePath: join(import.meta.dirname, 'template.1'), }, -}); + + generate, +}; diff --git a/packages/core/src/generators/metadata/index.mjs b/packages/core/src/generators/metadata/index.mjs index c03448d8..6c5fac0b 100644 --- a/packages/core/src/generators/metadata/index.mjs +++ b/packages/core/src/generators/metadata/index.mjs @@ -1,18 +1,21 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate, processChunk } from './generate.mjs'; /** * This generator generates a flattened list of metadata entries from a API doc * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'metadata', description: 'generates a flattened list of API doc metadata entries', - dependsOn: 'ast', + dependsOn: '@node-core/doc-kit/ast', hasParallelProcessor: true, -}); + + generate, + processChunk, +}; diff --git a/packages/core/src/generators/orama-db/index.mjs b/packages/core/src/generators/orama-db/index.mjs index cb189ad3..1052dcd2 100644 --- a/packages/core/src/generators/orama-db/index.mjs +++ b/packages/core/src/generators/orama-db/index.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; /** * This generator is responsible for generating the Orama database for the @@ -8,10 +8,12 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'orama-db', description: 'Generates the Orama database for the API docs.', - dependsOn: 'metadata', -}); + dependsOn: '@node-core/doc-kit/metadata', + + generate, +}; diff --git a/packages/core/src/generators/sitemap/index.mjs b/packages/core/src/generators/sitemap/index.mjs index d7b0773b..1c267699 100644 --- a/packages/core/src/generators/sitemap/index.mjs +++ b/packages/core/src/generators/sitemap/index.mjs @@ -1,21 +1,23 @@ 'use strict'; -import { createLazyGenerator } from '../../utils/generators.mjs'; +import { generate } from './generate.mjs'; /** * This generator generates a sitemap.xml file for search engine optimization * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'sitemap', description: 'Generates a sitemap.xml file for search engine optimization', - dependsOn: 'metadata', + dependsOn: '@node-core/doc-kit/metadata', defaultConfiguration: { indexURL: '{baseURL}/latest/api/', pageURL: '{indexURL}{path}.html', }, -}); + + generate, +}; diff --git a/packages/core/src/generators/types.d.ts b/packages/core/src/generators/types.d.ts index 278ada65..b729426a 100644 --- a/packages/core/src/generators/types.d.ts +++ b/packages/core/src/generators/types.d.ts @@ -28,7 +28,8 @@ declare global { } export interface ParallelTaskOptions { - generatorName: keyof AllGenerators; + /** Resolved import specifier the worker loads the generator from */ + generatorSpecifier: string; input: unknown[]; itemIndices: number[]; } @@ -67,7 +68,7 @@ declare global { description: string; - hasParallelProcessor: boolean; + hasParallelProcessor?: boolean; /** * The immediate generator that this generator depends on. @@ -90,8 +91,11 @@ declare global { * * The `ast-js` generator is the top-level parser for JavaScript files. It * passes the ASTs for any JavaScript files given in the input. + * + * Declared as an import specifier (e.g. `@node-core/doc-kit/metadata`), + * so a dependency may live in any installed package. */ - dependsOn: keyof AllGenerators | undefined; + dependsOn: string | undefined; /** * Generators are abstract and the different generators have different sort of inputs and outputs. diff --git a/packages/core/src/generators/web/__tests__/generate.test.mjs b/packages/core/src/generators/web/__tests__/generate.test.mjs index 94583d07..bb052f99 100644 --- a/packages/core/src/generators/web/__tests__/generate.test.mjs +++ b/packages/core/src/generators/web/__tests__/generate.test.mjs @@ -53,6 +53,7 @@ const createTestConfiguration = async context => { context.after(() => rm(output, { recursive: true, force: true })); const config = await setConfig({ + target: ['web'], output, version: 'v22.0.0', changelog: [], diff --git a/packages/core/src/generators/web/bundlers/__tests__/vite.test.mjs b/packages/core/src/generators/web/bundlers/__tests__/vite.test.mjs index fd17c5ae..7ff724c6 100644 --- a/packages/core/src/generators/web/bundlers/__tests__/vite.test.mjs +++ b/packages/core/src/generators/web/bundlers/__tests__/vite.test.mjs @@ -17,6 +17,7 @@ import { const output = join(tmpdir(), 'doc-kit-vite-test-output'); await setConfig({ + target: ['web'], output, version: 'v22.0.0', changelog: [], diff --git a/packages/core/src/generators/web/index.mjs b/packages/core/src/generators/web/index.mjs index c862321d..67e1ab4c 100644 --- a/packages/core/src/generators/web/index.mjs +++ b/packages/core/src/generators/web/index.mjs @@ -2,8 +2,8 @@ import { join } from 'node:path'; +import { generate } from './generate.mjs'; import { GITHUB_EDIT_URL } from '../../utils/configuration/templates.mjs'; -import { createLazyGenerator } from '../../utils/generators.mjs'; /** * Web generator - transforms JSX AST entries into complete web bundles. @@ -24,12 +24,12 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * * @type {import('./types').Generator} */ -export default createLazyGenerator({ +export default { name: 'web', description: 'Generates HTML/CSS/JS bundles from JSX AST entries', - dependsOn: 'jsx-ast', + dependsOn: '@node-core/doc-kit/jsx-ast', /** * @param {import('../../utils/configuration/types').Configuration} config @@ -42,9 +42,12 @@ export default createLazyGenerator({ editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`, pageURL: '{baseURL}/latest-{version}/api{path}.html', remoteConfigUrl: 'https://nodejs.org/site.json', - // By default, the search box is only shown when we are _also_ building search data + // By default, the search box is only shown when we are _also_ building + // search data. `target` holds resolved import specifiers, so match on the + // subpath rather than an exact name. showSearchBox: - Array.isArray(config.target) && config.target.includes('orama-db'), + Array.isArray(config.target) && + config.target.some(target => target.endsWith('orama-db')), // Project-specific document `` contents. `meta` and `links` are // arrays of attribute bags (boolean `true` renders a valueless attribute, @@ -99,4 +102,6 @@ export default createLazyGenerator({ // When omitted, the Vite adapter is loaded lazily during generation. bundler: undefined, }), -}); + + generate, +}; diff --git a/packages/core/src/generators/web/utils/__tests__/processing.test.mjs b/packages/core/src/generators/web/utils/__tests__/processing.test.mjs index 16d1fa6f..bd0b4c5b 100644 --- a/packages/core/src/generators/web/utils/__tests__/processing.test.mjs +++ b/packages/core/src/generators/web/utils/__tests__/processing.test.mjs @@ -12,6 +12,7 @@ import { } from '../processing.mjs'; await setConfig({ + target: ['web'], version: 'v22.0.0', changelog: [], generators: { diff --git a/packages/core/src/generators/web/utils/__tests__/relativeOrAbsolute.test.mjs b/packages/core/src/generators/web/utils/__tests__/relativeOrAbsolute.test.mjs index 4f8a2d7d..998d22e5 100644 --- a/packages/core/src/generators/web/utils/__tests__/relativeOrAbsolute.test.mjs +++ b/packages/core/src/generators/web/utils/__tests__/relativeOrAbsolute.test.mjs @@ -8,6 +8,7 @@ import { import { relativeOrAbsolute } from '../relativeOrAbsolute.mjs'; await setConfig({ + target: ['web'], version: 'v22.0.0', changelog: [], generators: { diff --git a/packages/core/src/threading/__tests__/parallel.test.mjs b/packages/core/src/threading/__tests__/parallel.test.mjs index 766e3d03..67671177 100644 --- a/packages/core/src/threading/__tests__/parallel.test.mjs +++ b/packages/core/src/threading/__tests__/parallel.test.mjs @@ -1,9 +1,13 @@ import { deepStrictEqual, ok, strictEqual } from 'node:assert'; import { describe, it } from 'node:test'; +import { loadGenerator } from '../../generators/loader.mjs'; import createWorkerPool from '../index.mjs'; import createParallelWorker from '../parallel.mjs'; +const metadataGenerator = await loadGenerator('metadata'); +const astJsGenerator = await loadGenerator('ast-js'); + /** * Helper to collect all results from an async generator. * @@ -41,7 +45,9 @@ async function collectChunks(generator) { describe('createParallelWorker', () => { it('should create a ParallelWorker with stream method', async () => { const pool = createWorkerPool(2); - const worker = createParallelWorker('metadata', pool, { threads: 2 }); + const worker = createParallelWorker('metadata', metadataGenerator, pool, { + threads: 2, + }); ok(worker); strictEqual(typeof worker.stream, 'function'); @@ -51,7 +57,7 @@ describe('createParallelWorker', () => { it('should handle empty items array', async () => { const pool = createWorkerPool(2); - const worker = createParallelWorker('ast-js', pool, { + const worker = createParallelWorker('ast-js', astJsGenerator, pool, { threads: 2, chunkSize: 10, }); @@ -65,7 +71,7 @@ describe('createParallelWorker', () => { it('should distribute items to multiple worker threads', async () => { const pool = createWorkerPool(4); - const worker = createParallelWorker('metadata', pool, { + const worker = createParallelWorker('metadata', metadataGenerator, pool, { threads: 4, chunkSize: 1, }); @@ -102,7 +108,7 @@ describe('createParallelWorker', () => { it('should yield results as chunks complete', async () => { const pool = createWorkerPool(2); - const worker = createParallelWorker('metadata', pool, { + const worker = createParallelWorker('metadata', metadataGenerator, pool, { threads: 2, chunkSize: 1, }); @@ -127,7 +133,7 @@ describe('createParallelWorker', () => { it('should work with single thread and items', async () => { const pool = createWorkerPool(2); - const worker = createParallelWorker('metadata', pool, { + const worker = createParallelWorker('metadata', metadataGenerator, pool, { threads: 2, chunkSize: 5, }); @@ -149,7 +155,7 @@ describe('createParallelWorker', () => { it('should use sliceInput for metadata generator', async () => { const pool = createWorkerPool(2); - const worker = createParallelWorker('metadata', pool, { + const worker = createParallelWorker('metadata', metadataGenerator, pool, { threads: 2, chunkSize: 1, }); diff --git a/packages/core/src/threading/chunk-worker.mjs b/packages/core/src/threading/chunk-worker.mjs index 08e363ac..02e30189 100644 --- a/packages/core/src/threading/chunk-worker.mjs +++ b/packages/core/src/threading/chunk-worker.mjs @@ -1,4 +1,4 @@ -import { allGenerators } from '../generators/index.mjs'; +import { loadGenerator } from '../generators/loader.mjs'; import { setConfig } from '../utils/configuration/index.mjs'; /** @@ -9,7 +9,7 @@ import { setConfig } from '../utils/configuration/index.mjs'; * @returns {Promise} The processed result */ export default async ({ - generatorName, + generatorSpecifier, input, itemIndices, extra, @@ -17,7 +17,7 @@ export default async ({ }) => { await setConfig(configuration); - const generator = allGenerators[generatorName]; + const generator = await loadGenerator(generatorSpecifier); return generator.processChunk(input, itemIndices, extra); }; diff --git a/packages/core/src/threading/parallel.mjs b/packages/core/src/threading/parallel.mjs index d74ba92e..b0e59f6c 100644 --- a/packages/core/src/threading/parallel.mjs +++ b/packages/core/src/threading/parallel.mjs @@ -1,6 +1,5 @@ 'use strict'; -import { allGenerators } from '../generators/index.mjs'; import logger from '../logger/index.mjs'; const parallelLogger = logger.child('parallel'); @@ -31,6 +30,7 @@ const createChunks = (count, size) => { * @param {number[]} indices - Indices to process * @param {Object} extra - Stuff to pass to the worker * @param {import('../utils/configuration/types').Configuration} configuration - Serialized options + * @param {string} generatorSpecifier - Resolved specifier of the generator * @param {string} generatorName - Name of the generator * @returns {ParallelTaskOptions} Task data for Piscina */ @@ -39,10 +39,11 @@ const createTask = ( indices, extra, configuration, + generatorSpecifier, generatorName ) => { return { - generatorName, + generatorSpecifier, // Only send the items needed for this chunk (reduces serialization overhead) input: indices.map(i => fullInput[i]), // Remap indices to 0-based for the sliced array @@ -58,19 +59,21 @@ const createTask = ( /** * Creates a parallel worker that distributes work across a Piscina thread pool. * - * @param {keyof AllGenerators} generatorName - Generator name + * @param {string} specifier - Resolved generator specifier (importable from workers) + * @param {GeneratorMetadata} generator - The loaded generator * @param {import('piscina').Piscina} pool - Piscina instance * @param {import('../utils/configuration/types').Configuration} configuration - Generator options * @returns {ParallelWorker} */ export default function createParallelWorker( - generatorName, + specifier, + generator, pool, configuration ) { const { threads, chunkSize } = configuration; - const generator = allGenerators[generatorName]; + const { name } = generator; return { /** @@ -90,7 +93,7 @@ export default function createParallelWorker( parallelLogger.debug( `Distributing ${items.length} items across ${chunks.length} chunks`, - { generator: generatorName, chunks: chunks.length, chunkSize, threads } + { generator: name, chunks: chunks.length, chunkSize, threads } ); const runInOneGo = threads <= 1 || items.length <= 2; @@ -108,7 +111,7 @@ export default function createParallelWorker( const promise = pool .run( - createTask(items, indices, extra, configuration, generatorName) + createTask(items, indices, extra, configuration, specifier, name) ) .then(result => ({ promise, result })); @@ -127,7 +130,7 @@ export default function createParallelWorker( completed++; parallelLogger.debug(`Chunk ${completed}/${chunks.length} completed`, { - generator: generatorName, + generator: name, }); yield result; diff --git a/packages/core/src/utils/__tests__/generators.test.mjs b/packages/core/src/utils/__tests__/generators.test.mjs index c511b3ec..cd0c48eb 100644 --- a/packages/core/src/utils/__tests__/generators.test.mjs +++ b/packages/core/src/utils/__tests__/generators.test.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { describe, it, mock, afterEach } from 'node:test'; +import { describe, it } from 'node:test'; import { groupNodesByModule, @@ -7,7 +7,6 @@ import { coerceSemVer, getCompatibleVersions, legacyToJSON, - createLazyGenerator, } from '../generators.mjs'; describe('groupNodesByModule', () => { @@ -124,38 +123,3 @@ describe('legacyToJSON', () => { assert.ok(result.includes('\n')); }); }); - -describe('createLazyGenerator', () => { - afterEach(() => mock.restoreAll()); - - it('spreads metadata properties onto the returned object', () => { - const metadata = { - name: 'ast', - description: 'Parses Markdown', - dependsOn: undefined, - }; - const gen = createLazyGenerator(metadata); - assert.equal(gen.name, 'ast'); - assert.equal(gen.description, 'Parses Markdown'); - }); - - it('exposes generate and processChunk functions that delegate to the lazily loaded module', async () => { - // Both exports are mocked in a single mock.module() call to avoid ESM import - // cache collisions that occur when re-mocking the same specifier across two it() blocks. - const specifier = import.meta.resolve('../../generators/ast/generate.mjs'); - const fakeGenerate = async input => `processed:${input}`; - const fakeProcessChunk = async (input, indices) => - indices.map(i => input[i]); - mock.module(specifier, { - namedExports: { generate: fakeGenerate, processChunk: fakeProcessChunk }, - }); - - const gen = createLazyGenerator({ name: 'ast' }); - - const generateResult = await gen.generate('hello'); - assert.equal(generateResult, 'processed:hello'); - - const processChunkResult = await gen.processChunk(['a', 'b', 'c'], [0, 2]); - assert.deepStrictEqual(processChunkResult, ['a', 'c']); - }); -}); diff --git a/packages/core/src/utils/configuration/__tests__/index.test.mjs b/packages/core/src/utils/configuration/__tests__/index.test.mjs index b32d5b19..a2cc957f 100644 --- a/packages/core/src/utils/configuration/__tests__/index.test.mjs +++ b/packages/core/src/utils/configuration/__tests__/index.test.mjs @@ -16,20 +16,29 @@ const createMockConfig = (overrides = {}) => ({ ...overrides, }); +// Synthetic generators keyed by specifier; the identity resolver below means +// shorthand names and specifiers are the same thing in these tests. +const mockGenerators = { + json: { name: 'json', defaultConfiguration: { format: 'json' } }, + html: { name: 'html', defaultConfiguration: { format: 'html' } }, + markdown: { name: 'markdown' }, + web: { + name: 'web', + defaultConfiguration: config => ({ + showSearchBox: + Array.isArray(config.target) && config.target.includes('orama-db'), + }), + }, +}; + // Mock modules -mock.module('../../../generators/index.mjs', { +mock.module('../../../generators/loader.mjs', { namedExports: { - allGenerators: { - json: { defaultConfiguration: { format: 'json' } }, - html: { defaultConfiguration: { format: 'html' } }, - markdown: {}, - web: { - defaultConfiguration: config => ({ - showSearchBox: - Array.isArray(config.target) && config.target.includes('orama-db'), - }), - }, - }, + resolveGeneratorSpecifier: specifier => specifier, + loadGenerator: async specifier => mockGenerators[specifier], + // Defaults are computed from the loaded generators; returning the full + // set regardless of targets keeps the assertions below simple. + loadGenerators: async () => new Map(Object.entries(mockGenerators)), }, }); mock.module('../../../parsers/markdown.mjs', { diff --git a/packages/core/src/utils/configuration/index.mjs b/packages/core/src/utils/configuration/index.mjs index d349b714..965cfab5 100644 --- a/packages/core/src/utils/configuration/index.mjs +++ b/packages/core/src/utils/configuration/index.mjs @@ -5,26 +5,32 @@ import { cosmiconfig } from 'cosmiconfig'; import { coerce } from 'semver'; import { CHANGELOG_URL, populate } from './templates.mjs'; -import { allGenerators } from '../../generators/index.mjs'; +import { + loadGenerators, + resolveGeneratorSpecifier, +} from '../../generators/loader.mjs'; import logger from '../../logger/index.mjs'; import { parseChangelog, parseIndex } from '../../parsers/markdown.mjs'; import { enforceArray } from '../array.mjs'; import { leftHandAssign } from '../generators.mjs'; -import { deepMerge, lazy } from '../misc.mjs'; +import { deepMerge } from '../misc.mjs'; const configExplorer = cosmiconfig('doc-kit'); /** - * Get's the default configuration + * Get's the default configuration for the loaded generators + * + * @param {Map} generators - Loaded generators + * @param {Partial} config - The user configuration */ -export const getDefaultConfig = lazy(config => - Object.keys(allGenerators).reduce( - (acc, k) => { - acc[k] = - 'defaultConfiguration' in allGenerators[k] - ? typeof allGenerators[k].defaultConfiguration === 'function' - ? allGenerators[k].defaultConfiguration(config) - : allGenerators[k].defaultConfiguration +export const getDefaultConfig = (generators, config) => + [...generators.values()].reduce( + (acc, generator) => { + acc[generator.name] = + 'defaultConfiguration' in generator + ? typeof generator.defaultConfiguration === 'function' + ? generator.defaultConfiguration(config) + : generator.defaultConfiguration : {}; return acc; @@ -50,8 +56,7 @@ export const getDefaultConfig = lazy(config => threads: process.arch === 'riscv64' ? 1 : cpus().length, chunkSize: 10, }) - ) -); + ); /** * Loads an explicit configuration file or searches for one using cosmiconfig. @@ -150,7 +155,16 @@ export const createRunConfiguration = async options => { // Resolve user configuration first so dynamic defaults can use it const cliConfig = createConfigFromCLIOptions(options); const intermediate = deepMerge(config, cliConfig); - const merged = deepMerge(getDefaultConfig(intermediate), intermediate); + + // Resolve shorthand targets into import specifiers, then load the requested + // generators (and their dependency closure) so their defaults can be applied + intermediate.target &&= intermediate.target.map(resolveGeneratorSpecifier); + const generators = await loadGenerators(intermediate.target ?? []); + + const merged = deepMerge( + getDefaultConfig(generators, intermediate), + intermediate + ); // These need to be coerced merged.threads = Math.max(merged.threads, 1); @@ -169,8 +183,8 @@ export const createRunConfiguration = async options => { // Now assign to each generator config (they inherit from global) await Promise.all( - Object.keys(allGenerators).map(async k => { - const value = merged[k]; + [...generators.values()].map(async ({ name }) => { + const value = merged[name]; // Transform generator-specific overrides await transformConfig(value); diff --git a/packages/core/src/utils/generators.mjs b/packages/core/src/utils/generators.mjs index fae6258e..48430853 100644 --- a/packages/core/src/utils/generators.mjs +++ b/packages/core/src/utils/generators.mjs @@ -2,8 +2,6 @@ import { coerce, major } from 'semver'; -import { lazy } from './misc.mjs'; - /** * Groups all the API metadata nodes by module (`api` property) so that we can process each different file * based on the module it belongs to. @@ -123,30 +121,3 @@ export const legacyToJSON = ( }, ...args ); - -/** - * Creates a generator with the provided metadata. - * @template T - * @param {T} metadata - The metadata object - * @returns {Promise} The metadata object with generator methods - */ -export const createLazyGenerator = metadata => { - const generator = lazy( - () => import(`../generators/${metadata.name}/generate.mjs`) - ); - return { - ...metadata, - /** - * Processes a chunk using the lazily-loaded generator. - * @param {...any} args - Arguments to pass to the processChunk method - * @returns {Promise} Result from the generator's processChunk method - */ - processChunk: async (...args) => (await generator()).processChunk(...args), - /** - * Generates output using the lazily-loaded generator. - * @param {...any} args - Arguments to pass to the generate method - * @returns {Promise} Result from the generator's generate method - */ - generate: async (...args) => (await generator()).generate(...args), - }; -}; diff --git a/packages/core/src/generators/legacy-json/utils/__tests__/parseList.test.mjs b/packages/core/src/utils/signature/__tests__/parseList.test.mjs similarity index 100% rename from packages/core/src/generators/legacy-json/utils/__tests__/parseList.test.mjs rename to packages/core/src/utils/signature/__tests__/parseList.test.mjs diff --git a/packages/core/src/generators/legacy-json/utils/__tests__/parseSignature.test.mjs b/packages/core/src/utils/signature/__tests__/parseSignature.test.mjs similarity index 100% rename from packages/core/src/generators/legacy-json/utils/__tests__/parseSignature.test.mjs rename to packages/core/src/utils/signature/__tests__/parseSignature.test.mjs diff --git a/packages/core/src/utils/signature/constants.mjs b/packages/core/src/utils/signature/constants.mjs new file mode 100644 index 00000000..16b524b2 --- /dev/null +++ b/packages/core/src/utils/signature/constants.mjs @@ -0,0 +1,12 @@ +// Grabs a method's name +export const NAME_EXPRESSION = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/; + +// Checks if there's a leading hyphen +export const LEADING_HYPHEN = /^-\s*/; + +// Grabs the default value if present +export const DEFAULT_EXPRESSION = /\s*\*\*Default:\*\*\s*([^]+)$/i; + +// Grabs the parameters from a method's signature +// ex/ 'new buffer.Blob([sources[, options]])'.match(PARAM_EXPRESSION) === ['([sources[, options]])', '[sources[, options]]'] +export const PARAM_EXPRESSION = /\(([^)]+)\);?$/; diff --git a/packages/core/src/generators/legacy-json/utils/parseList.mjs b/packages/core/src/utils/signature/parseList.mjs similarity index 92% rename from packages/core/src/generators/legacy-json/utils/parseList.mjs rename to packages/core/src/utils/signature/parseList.mjs index 75936e15..9839b594 100644 --- a/packages/core/src/generators/legacy-json/utils/parseList.mjs +++ b/packages/core/src/utils/signature/parseList.mjs @@ -2,11 +2,11 @@ import { DEFAULT_EXPRESSION, LEADING_HYPHEN, NAME_EXPRESSION, -} from '../constants.mjs'; +} from './constants.mjs'; import parseSignature from './parseSignature.mjs'; -import { leftHandAssign } from '../../../utils/generators.mjs'; -import { QUERIES, UNIST } from '../../../utils/queries/index.mjs'; -import { transformNodesToString } from '../../../utils/unist.mjs'; +import { leftHandAssign } from '../generators.mjs'; +import { QUERIES, UNIST } from '../queries/index.mjs'; +import { transformNodesToString } from '../unist.mjs'; /** * Extracts and removes a specific pattern from a text string while storing the result in a key of the `current` object. @@ -31,7 +31,7 @@ export const extractPattern = (text, pattern, key, current) => { * Parses an individual list item node to extract its properties * * @param {import('@types/mdast').ListItem} child - * @returns {import('../types').ParameterList} + * @returns {import('./types').ParameterList} */ export function parseListItem(child) { const current = {}; @@ -85,7 +85,7 @@ export function parseListItem(child) { /** * Parses a list of nodes and updates the corresponding section object with the extracted information. * Handles different section types such as methods, properties, and events differently. - * @param {import('../types').Section} section + * @param {import('../../generators/legacy-json/types').Section} section * @param {import('@types/mdast').RootContent[]} nodes */ export function parseList(section, nodes) { diff --git a/packages/core/src/generators/legacy-json/utils/parseSignature.mjs b/packages/core/src/utils/signature/parseSignature.mjs similarity index 90% rename from packages/core/src/generators/legacy-json/utils/parseSignature.mjs rename to packages/core/src/utils/signature/parseSignature.mjs index e71ebe24..053daa93 100644 --- a/packages/core/src/generators/legacy-json/utils/parseSignature.mjs +++ b/packages/core/src/utils/signature/parseSignature.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { PARAM_EXPRESSION } from '../constants.mjs'; +import { PARAM_EXPRESSION } from './constants.mjs'; const OPTIONAL_LEVEL_CHANGES = { '[': 1, ']': -1 }; @@ -78,8 +78,8 @@ export function parseDefaultValue(parameterName) { /** * @param {string} parameterName * @param {number} index - * @param {Array} markdownParameters - * @returns {import('../types.d.ts').Parameter} + * @param {Array} markdownParameters + * @returns {import('./types.d.ts').Parameter} */ export function findParameter(parameterName, index, markdownParameters) { const parameter = markdownParameters[index]; @@ -109,11 +109,11 @@ export function findParameter(parameterName, index, markdownParameters) { /** * @param {string[]} declaredParameters - * @param {Array} markdownParameters + * @param {Array} markdownParameters */ export function parseParameters(declaredParameters, markdownParameters) { /** - * @type {Array} + * @type {Array} */ let parameters = []; @@ -166,12 +166,12 @@ export function parseParameters(declaredParameters, markdownParameters) { /** * @param {string} textRaw Something like `new buffer.Blob([sources[, options]])` - * @param {Array { /** - * @type {import('../types.d.ts').MethodSignature} + * @type {import('./types.d.ts').MethodSignature} */ const signature = { params: [] }; diff --git a/packages/core/src/utils/signature/types.d.ts b/packages/core/src/utils/signature/types.d.ts new file mode 100644 index 00000000..c02b7d86 --- /dev/null +++ b/packages/core/src/utils/signature/types.d.ts @@ -0,0 +1,66 @@ +/** + * Represents a parameter for methods or functions. + */ +export interface Parameter { + /** + * The name of the parameter. + */ + name: string; + + /** + * Indicates if the parameter is optional. + */ + optional?: boolean; + + /** + * The default value for the parameter. + */ + default?: string; +} + +/** + * Represents a method signature, including its parameters and return type. + */ +export interface MethodSignature { + /** + * A list of parameters for the method. + */ + params: Parameter[]; + + /** + * The return type of the method. + */ + return?: Parameter; +} + +/** + * Represents a list of parameters. + */ +export interface ParameterList { + /** + * Raw parameter description + */ + textRaw: string; + + /** + * A short description of the parameter. + */ + desc?: string; + + /** + * The name of the parameter. + */ + name: string; + + /** + * The type of the parameter (E.G. string, boolean). + */ + type?: string; + + /** + * The default value. + */ + default?: string; + + options?: ParameterList; +}