Skip to content

Commit 5d2c825

Browse files
ovflowdflakey5canerakdas
authored
feat: introduce based generator logic (#42)
* feat: introduce based generator logic * chore: actually write the file * chore: fixed typos * Update src/generators.mjs Co-authored-by: flakey5 <[email protected]> * chore: explicitly import type * chore: better types for the generators * chore: more type optimisations * chore: minor typo * chore: even more type coersion and use plain object instead of Map * chore: type directly on the generator * chore: fix typo * Apply suggestions from code review Co-authored-by: Caner Akdas <[email protected]> * chore: code review --------- Co-authored-by: flakey5 <[email protected]> Co-authored-by: Caner Akdas <[email protected]>
1 parent 3426a44 commit 5d2c825

File tree

9 files changed

+217
-11
lines changed

9 files changed

+217
-11
lines changed

src/generators.mjs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
3+
import availableGenerators from './generators/index.mjs';
4+
5+
/**
6+
* @typedef {import('./types.d.ts').ApiDocMetadataEntry} ApiDocMetadataEntry Local type alias for the API doc metadata entry
7+
* @typedef {{ ast: import('./generators/types.d.ts').GeneratorMetadata<ApiDocMetadataEntry, ApiDocMetadataEntry>}} AstGenerator The AST "generator" is a facade for the AST tree and it isn't really a generator
8+
* @typedef {import('./generators/types.d.ts').AvailableGenerators & AstGenerator} AllGenerators A complete set of the available generators, including the AST one
9+
*
10+
* This method creates a system that allows you to register generators
11+
* and then execute them in a specific order, keeping track of the
12+
* generation process, and handling errors that may occur from the
13+
* execution of generating content.
14+
*
15+
* When the final generator is reached, the system will return the
16+
* final generated content.
17+
*
18+
* Generators can output content that can be consumed by other generators;
19+
* Generators can also write to files. These would usually be considered
20+
* the final generators in the chain.
21+
*
22+
* @param {ApiDocMetadataEntry} input The parsed API doc metadata entries
23+
*/
24+
const createGenerator = input => {
25+
/**
26+
* We store all the registered generators to be processed
27+
* within a Record, so we can access their results at any time whenever needed
28+
* (we store the Promises of the generator outputs)
29+
*
30+
* @type {{ [K in keyof AllGenerators]: ReturnType<AllGenerators[K]['generate']> }}
31+
*/
32+
const cachedGenerators = { ast: Promise.resolve(input) };
33+
34+
/**
35+
* Runs the Generator engine with the provided top-level input and the given generator options
36+
*
37+
* @param {import('./generators/types.d.ts').GeneratorOptions} options The options for the generator runtime
38+
*/
39+
const runGenerators = async ({ output, generators }) => {
40+
// Note that this method is blocking, and will only execute one generator per-time
41+
// but it ensures all dependencies are resolved, and that multiple bottom-level generators
42+
// can reuse the already parsed content from the top-level/dependency generators
43+
for (const generatorName of generators) {
44+
const { dependsOn, generate } = availableGenerators[generatorName];
45+
46+
// If the generator dependency has not yet been resolved, we resolve
47+
// the dependency first before running the current generator
48+
if (dependsOn && dependsOn in cachedGenerators === false) {
49+
await runGenerators({ output, generators: [dependsOn] });
50+
}
51+
52+
// Ensures that the dependency output gets resolved before we run the current
53+
// generator with its dependency output as the input
54+
const dependencyOutput = await cachedGenerators[dependsOn];
55+
56+
// Adds the current generator execution Promise to the cache
57+
cachedGenerators[generatorName] = generate(dependencyOutput, { output });
58+
}
59+
60+
// Returns the value of the last generator of the current pipeline
61+
// Note that dependencies will be awaited (as shown on line 48)
62+
return cachedGenerators[generators[generators.length - 1]];
63+
};
64+
65+
return { runGenerators };
66+
};
67+
68+
export default createGenerator;

src/generators/index.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
3+
import jsonSimple from './json-simple/index.mjs';
4+
import legacyHtml from './legacy-html/index.mjs';
5+
6+
export default { jsonSimple, legacyHtml };

src/generators/json-simple/index.mjs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
3+
import { writeFile } from 'node:fs/promises';
4+
import { join } from 'node:path';
5+
6+
/**
7+
* This generator generates a simplified JSON version of the API docs and returns it as a string
8+
* this is not meant to be used for the final API docs, but for debugging and testing purposes
9+
*
10+
* This generator is a top-level generator, and it takes the raw AST tree of the API doc files
11+
* and returns a stringified JSON version of the API docs.
12+
*
13+
* @typedef {import('../../types.d.ts').ApiDocMetadataEntry[]} Input
14+
*
15+
* @type {import('../types.d.ts').GeneratorMetadata<Input, string>}
16+
*/
17+
export default {
18+
name: 'jsonSimple',
19+
20+
version: '1.0.0',
21+
22+
description:
23+
'Generates the simple JSON version of the API docs, and returns it as a string',
24+
25+
dependsOn: 'ast',
26+
27+
async generate(input, options) {
28+
// This simply grabs all the different files and stringifies them
29+
const stringifiedContent = JSON.stringify(input, null, 2);
30+
31+
// Writes all the API docs stringified content into one file
32+
// Note: The full JSON generator in the future will create one JSON file per top-level API doc file
33+
await writeFile(
34+
join(options.output, 'api-docs.json'),
35+
stringifiedContent,
36+
'utf-8'
37+
);
38+
},
39+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!.gitignore

src/generators/legacy-html/index.mjs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict';
2+
3+
/**
4+
* This generator generates the legacy HTML pages of the legacy API docs
5+
* for retro-compatibility and while we are implementing the new 'react' and 'html' generators.
6+
*
7+
* This generator is a top-level generator, and it takes the raw AST tree of the API doc files
8+
* and generates the HTML files to the specified output directory from the configuration settings
9+
*
10+
* @typedef {import('../../types.d.ts').ApiDocMetadataEntry[]} Input
11+
*
12+
* @type {import('../types.d.ts').GeneratorMetadata<Input, void>}
13+
*/
14+
export default {
15+
name: 'legacyHtml',
16+
17+
version: '1.0.0',
18+
19+
description:
20+
'Generates the legacy version of the API docs in HTML, with the assets and styles included as files',
21+
22+
dependsOn: 'ast',
23+
24+
async generate() {
25+
throw new Error('Not yet implemented');
26+
},
27+
};

src/generators/types.d.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type availableGenerators from './index.mjs';
2+
3+
// All available generators as an inferable type, to allow Generator interfaces
4+
// to be type complete and runtime friendly within `runGenerators`
5+
export type AvailableGenerators = typeof availableGenerators;
6+
7+
// This is the runtime config passed to the API doc generators
8+
export interface GeneratorOptions {
9+
// The path used to output generated files, this is to be considered
10+
// the base path that any generator will use for generating files
11+
// This parameter accepts globs but when passed to generators will contain
12+
// the already resolved absolute path to the output folder
13+
output: string;
14+
// A list of generators to be used in the API doc generation process;
15+
// This is considered a "sorted" list of generators, in the sense that
16+
// if the last entry of this list contains a generated value, we will return
17+
// the value of the last generator in the list, if any.
18+
generators: (keyof AvailableGenerators)[];
19+
}
20+
21+
export interface GeneratorMetadata<I extends any, O extends any> {
22+
// The name of the Generator. Must match the Key in the AvailableGenerators
23+
name: keyof AvailableGenerators;
24+
25+
version: string;
26+
27+
description: string;
28+
29+
/**
30+
* The immediate generator that this generator depends on.
31+
* For example, the `html` generator depends on the `react` generator.
32+
*
33+
* If a given generator has no "before" generator, it will be considered a top-level
34+
* generator, and run in parallel.
35+
*
36+
* Assume you pass to the `createGenerator`: ['json', 'html'] as the generators,
37+
* this means both the 'json' and the 'html' generators will be executed and generate their
38+
* own outputs in parallel. If the 'html' generator depends on the 'react' generator, then
39+
* the 'react' generator will be executed first, then the 'html' generator.
40+
*
41+
* But both 'json' and 'html' generators will be executed in parallel.
42+
*
43+
* If you pass `createGenerator` with ['react', 'html'], the 'react' generator will be executed first,
44+
* as it is a top level generator and then the 'html' generator would be executed after the 'react' generator.
45+
*
46+
* The 'ast' generator is the top-level parser, and if 'ast' is passed to `dependsOn`, then the generator
47+
* will be marked as a top-level generator.
48+
*/
49+
dependsOn: keyof AvailableGenerators | 'ast';
50+
51+
/**
52+
* Generators are abstract and the different generators have different sort of inputs and outputs.
53+
* For example, a MDX generator would take the raw AST and output MDX with React Components;
54+
* Whereas a JSON generator would take the raw AST and output JSON;
55+
* Then a React generator could receive either the raw AST or the MDX output and output React Components.
56+
* (depending if they support such I/O)
57+
*
58+
* Hence you can combine different generators to achieve different outputs.
59+
*/
60+
generate: (input: I, options: Partial<GeneratorOptions>) => Promise<O>;
61+
}

src/metadata.mjs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
'use strict';
22

33
/**
4+
* @typedef {import('./types.d.ts').ApiDocMetadataEntry} ApiDocMetadataEntry Local type alias for the API doc metadata entry
5+
* @typedef {import('./types.d.ts').ApiDocRawMetadataEntry} ApiDocRawMetadataEntry Local type alias for the API doc raw metadata entry
6+
* @typedef {import('./types.d.ts').HeadingMetadataEntry} HeadingMetadataEntry Local type alias for the heading metadata entry
7+
*
48
* This method allows us to handle creation of Metadata entries
59
* within the current scope of API docs being parsed
610
*
@@ -15,9 +19,9 @@ const createMetadata = slugger => {
1519
* transformed into NavigationEntries and MetadataEntries
1620
*
1721
* @type {{
18-
* heading: import('./types.d.ts').HeadingMetadataEntry,
19-
* stability: import('./types.d.ts').ApiDocMetadataEntry['stability'],
20-
* properties: import('./types.d.ts').ApiDocRawMetadataEntry,
22+
* heading: HeadingMetadataEntry,
23+
* properties: ApiDocRawMetadataEntry,
24+
* stability: ApiDocMetadataEntry['stability'],
2125
* }}
2226
*/
2327
const internalMetadata = {
@@ -27,23 +31,23 @@ const createMetadata = slugger => {
2731
name: undefined,
2832
depth: -1,
2933
},
30-
stability: undefined,
3134
properties: {},
35+
stability: undefined,
3236
};
3337

3438
return {
3539
/**
3640
* Set the Heading of a given Metadata
3741
*
38-
* @param {import('./types.d.ts').HeadingMetadataEntry} heading The new heading metadata
42+
* @param {HeadingMetadataEntry} heading The new heading metadata
3943
*/
4044
setHeading: heading => {
4145
internalMetadata.heading = heading;
4246
},
4347
/**
4448
* Set the Stability Index of a given Metadata
4549
*
46-
* @param {import('./types.d.ts').ApiDocMetadataEntry['stability']} stability The new stability metadata
50+
* @param {ApiDocMetadataEntry['stability']} stability The new stability metadata
4751
*/
4852
setStability: stability => {
4953
internalMetadata.stability = stability;
@@ -57,7 +61,7 @@ const createMetadata = slugger => {
5761
* meaning that this method can be called multiple times to update the properties
5862
* and complement each set of data.
5963
*
60-
* @param {Partial<import('./types.d.ts').ApiDocRawMetadataEntry>} properties Extra Metadata properties to be defined
64+
* @param {Partial<ApiDocRawMetadataEntry>} properties Extra Metadata properties to be defined
6165
*/
6266
updateProperties: properties => {
6367
internalMetadata.properties = {
@@ -74,8 +78,8 @@ const createMetadata = slugger => {
7478
* as it can be manipulated outside of the scope of the generation of the content
7579
*
7680
* @param {import('vfile').VFile} apiDoc The API doc file being parsed
77-
* @param {import('./types.d.ts').ApiDocMetadataEntry['content']} section An AST tree containing the Nodes of the API doc entry section
78-
* @returns {import('./types.d.ts').ApiDocMetadataEntry} The locally created Metadata entries
81+
* @param {ApiDocMetadataEntry['content']} section An AST tree containing the Nodes of the API doc entry section
82+
* @returns {ApiDocMetadataEntry} The locally created Metadata entries
7983
*/
8084
create: (apiDoc, section) => {
8185
// This is the ID of a certain Navigation entry, which allows us to anchor

src/parser.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ const createParser = () => {
111111

112112
// This is the cutover index of the subtree that we should get
113113
// of all the Nodes within the AST tree that belong to this section
114-
// If `next` is equals the current heading, it means ther's no next heading
114+
// If `next` is equals the current heading, it means there's no next heading
115115
// and we are reaching the end of the document, hence the cutover should be the end of
116116
// the document itself.
117117
const stop =

src/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Parent, Node } from 'unist';
22

33
// String serialization of the AST tree
44
// @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior
5-
export interface WithJSON<T extends Node, J = any> extends T {
5+
export interface WithJSON<T extends Node, J extends any = any> extends T {
66
toJSON: () => J;
77
}
88

0 commit comments

Comments
 (0)