-
Notifications
You must be signed in to change notification settings - Fork 9
feat: legacy json generator #92
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
Changes from all commits
46e2c4b
a51a492
6a0d6fb
08c1365
e3c8640
b95dafb
0fa285d
1444056
5a61eb7
5d084b4
5418f01
8e50047
ee13845
ffdfe06
360786d
858d77f
a27d69b
4a44ab7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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; | ||
}, | ||
}; |
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>; | ||
} |
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 = /\((.+)\);?$/; |
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we use a for here? To avoid using Promise.all? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Unfortunately I don't remember much about it, but I'd seen a blog post about it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
}, | ||
}; |
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; | ||
} |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Wdym? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking you can use I think I could explain this over a call, but pretty much using the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.