Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: legacy json generator #92

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"html-minifier-terser": "^7.2.0",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.0",
"remark-html": "^16.0.1",
AugustinMauroy marked this conversation as resolved.
Show resolved Hide resolved
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"remark-stringify": "^11.0.0",
Expand Down
4 changes: 4 additions & 0 deletions src/generators/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import jsonSimple from './json-simple/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';

export default {
'json-simple': jsonSimple,
'legacy-html': legacyHtml,
'legacy-html-all': legacyHtmlAll,
'legacy-json': legacyJson,
'legacy-json-all': legacyJsonAll,
};
58 changes: 58 additions & 0 deletions src/generators/legacy-json-all/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use strict';

import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';

/**
* @typedef {Array<import('../legacy-json/types.d.ts').Section>} Input
*
* @type {import('../types.d.ts').GeneratorMetadata<Input, import('./types.d.ts').Output>}
*/
export default {
name: 'legacy-json-all',

version: '1.0.0',

description:
'Generates the `all.json` file from the `legacy-json` generator, which includes all the modules in one single file.',

dependsOn: 'legacy-json',

async generate(input, { output }) {
/**
* @type {import('./types.d.ts').Output}
*/
const generatedValue = {
miscs: [],
modules: [],
classes: [],
globals: [],
methods: [],
};

const propertiesToCopy = [
'miscs',
'modules',
'classes',
'globals',
'methods',
];

input.forEach(section => {
// Copy the relevant properties from each section into our output
propertiesToCopy.forEach(property => {
if (section[property]) {
generatedValue[property].push(...section[property]);
}
});
});

await writeFile(
join(output, 'all.json'),
JSON.stringify(generatedValue),
'utf8'
);

return generatedValue;
},
};
14 changes: 14 additions & 0 deletions src/generators/legacy-json-all/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
MiscSection,
Section,
SignatureSection,
ModuleSection,
} from '../legacy-json/types';

export interface Output {
miscs: Array<MiscSection>;
modules: Array<Section>;
classes: Array<SignatureSection>;
globals: Array<ModuleSection | { type: 'global' }>;
methods: Array<SignatureSection>;
}
18 changes: 18 additions & 0 deletions src/generators/legacy-json/constants.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Grabs a method's return value
export const RETURN_EXPRESSION = /^returns?\s*:?\s*/i;

// Grabs a method's name
export const NAME_EXPRESSION = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/;

// Denotes a method's type
export const TYPE_EXPRESSION = /^\{([^}]+)\}\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 = /\((.+)\);?$/;
68 changes: 68 additions & 0 deletions src/generators/legacy-json/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use strict';

import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { groupNodesByModule } from '../../utils/generators.mjs';
import buildSection from './utils/buildSection.mjs';

/**
* This generator is responsible for generating the legacy JSON files for the
* legacy API docs for retro-compatibility. It is to be replaced while we work
* on the new schema for this file.
*
* This is a top-level generator, intaking the raw AST tree of the api docs.
* It generates JSON files to the specified output directory given by the
* config.
*
* @typedef {Array<ApiDocMetadataEntry>} Input
*
* @type {import('../types.d.ts').GeneratorMetadata<Input, import('./types.d.ts').Section[]>}
*/
export default {
name: 'legacy-json',

version: '1.0.0',

description: 'Generates the legacy version of the JSON API docs.',

dependsOn: 'ast',

async generate(input, { output }) {
// This array holds all the generated values for each module
const generatedValues = [];

const groupedModules = groupNodesByModule(input);

// Gets the first nodes of each module, which is considered the "head"
const headNodes = input.filter(node => node.heading.depth === 1);

/**
* @param {ApiDocMetadataEntry} head
* @returns {import('./types.d.ts').ModuleSection}
*/
const processModuleNodes = head => {
const nodes = groupedModules.get(head.api);

const section = buildSection(head, nodes);
generatedValues.push(section);

return section;
};

await Promise.all(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use a for here? To avoid using Promise.all?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what was originally done, then after an initial review, we put a Promise.all instead of a series of awaits in a loop.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I stuttering O.o -- I still believe await would be better here for readability

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in terms of code readability, it's the same for me. But from what I've been able to understand in this kind of case it's better for performance to use Promise.all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been able to understand in this kind of case it's better for performance to use Promise.all.

Why?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Unfortunately I don't remember much about it, but I'd seen a blog post about it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, but that would only be the case if there's threading. It is correct to say that with the Promise approach at least the Promises will all be dispatched in parallel, but they will be executed one per time.

So in the end the feeling might be that it is faster, although it might not be. I do believe await syntax would better suit here, but no strong feelings.

headNodes.map(async node => {
// Get the json for the node's section
const section = processModuleNodes(node);

// Write it to the output file
await writeFile(
join(output, `${node.api}.json`),
JSON.stringify(section),
'utf8'
);
})
);

return generatedValues;
},
};
83 changes: 83 additions & 0 deletions src/generators/legacy-json/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ListItem } from 'mdast';

export interface HierarchizedEntry extends ApiDocMetadataEntry {
hierarchyChildren: Array<ApiDocMetadataEntry>;
}

export interface Meta {
changes: Array<ApiDocMetadataChange>;
added?: Array<string>;
napiVersion?: Array<string>;
deprecated?: Array<string>;
removed?: Array<string>;
}

export interface SectionBase {
type: string;
name: string;
textRaw: string;
displayName?: string;
desc: string;
shortDesc?: string;
stability?: number;
stabilityText?: string;
meta?: Meta;
}

export interface ModuleSection extends SectionBase {
type: 'module';
source: string;
miscs?: Array<MiscSection>;
modules?: Array<ModuleSection>;
classes?: Array<SignatureSection>;
methods?: Array<MethodSignature>;
properties?: Array<PropertySection>;
globals?: ModuleSection | { type: 'global' };
signatures?: Array<SignatureSection>;
}

export interface SignatureSection extends SectionBase {
type: 'class' | 'ctor' | 'classMethod' | 'method';
signatures: Array<MethodSignature>;
}

export type Section =
| SignatureSection
| PropertySection
| EventSection
| MiscSection;

export interface Parameter {
name: string;
optional?: boolean;
default?: string;
}

export interface MethodSignature {
params: Array<Parameter>;
return?: string;
}

export interface PropertySection extends SectionBase {
type: 'property';
[key: string]: string | undefined;
}

export interface EventSection extends SectionBase {
type: 'event';
params: Array<ListItem>;
}

export interface MiscSection extends SectionBase {
type: 'misc';
[key: string]: string | undefined;
}

export interface List {
textRaw: string;
desc?: string;
name: string;
type?: string;
default?: string;
options?: List;
}
63 changes: 63 additions & 0 deletions src/generators/legacy-json/utils/buildHierarchy.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* We need the files to be in a hierarchy based off of depth, but they're
* given to us flattened. So, let's fix that.
*
* Assuming that {@link entries} is in the same order as the elements are in
* the markdown, we can use the entry's depth property to reassemble the
* hierarchy.
*
* If depth <= 1, it's a top-level element (aka a root).
*
* If it's depth is greater than the previous entry's depth, it's a child of
* the previous entry. Otherwise (if it's less than or equal to the previous
* entry's depth), we need to find the entry that it was the greater than. We
* can do this by just looping through entries in reverse starting at the
* current index - 1.
*
* @param {Array<ApiDocMetadataEntry>} entries
* @returns {Array<import('../types.d.ts').HierarchizedEntry>}
*/
export function buildHierarchy(entries) {
const roots = [];

for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const currentDepth = entry.heading.depth;

if (currentDepth <= 1) {
// We're a top-level entry
roots.push(entry);
continue;
}

const previousEntry = entries[i - 1];

const previousDepth = previousEntry.heading.depth;
Copy link
Member

@ovflowd ovflowd Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting way of iterating this. Wouldn't it be easier to use the heading section topology we already created, which has heading info that tells which depth you are based on the heading depth?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be easier to use the heading section topology we already created

Wdym?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking you can use unified here, to iterate through the headings, (which are nodes) and then automatically add children to each parent.

I think I could explain this over a call, but pretty much using the visit API to self reference the previous heading in terms of depth, and then visit next levels.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My fear is that the code below is too much convoluted and hacky 🙈

if (currentDepth > previousDepth) {
// We're a child of the previous one
if (previousEntry.hierarchyChildren === undefined) {
previousEntry.hierarchyChildren = [];
}

previousEntry.hierarchyChildren.push(entry);
} else {
if (i < 2) {
throw new Error(`can't find parent since i < 2 (${i})`);
}

// Loop to find the entry we're a child of
for (let j = i - 2; j >= 0; j--) {
flakey5 marked this conversation as resolved.
Show resolved Hide resolved
const jEntry = entries[j];
const jDepth = jEntry.heading.depth;

if (currentDepth > jDepth) {
// Found it
jEntry.hierarchyChildren.push(entry);
break;
}
}
}
}

return roots;
}
Loading