From 5cdb7e09c0a45174b82eede50de377521ebe70f3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 21 Nov 2023 12:31:48 +0100 Subject: [PATCH] feat: handle nested submodules (This is a re-roll of #1190). `jsii-docgen` used to assume that submodules only went one level deep, i.e. that there could not be submodules within submodules. Break that assumption by doing the following: - Use `assembly.allSubmodules` everywhere `assembly.submodules` used to be used. - Address submodules by FQN instead of by `name` (which only holds the last name component). - As an exception: `documentation.toJson()` accepts both FQN as well as root-relative name for backwards compatibility. --- src/cli.ts | 9 ++-- src/docgen/render/markdown-render.ts | 4 +- src/docgen/transpile/transpile.ts | 11 ++++- src/docgen/view/api-reference.ts | 6 +-- src/docgen/view/documentation.ts | 69 ++++++++++++++++++++-------- 5 files changed, 69 insertions(+), 30 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 34bfd928..f30673ad 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,7 @@ import * as fs from 'fs/promises'; import * as path from 'node:path'; import * as yargs from 'yargs'; -import { Language } from './docgen/transpile/transpile'; +import { Language, submoduleRelName } from './docgen/transpile/transpile'; import { Documentation } from './index'; type GenerateOptions = { @@ -41,12 +41,12 @@ async function generateForLanguage(docs: Documentation, options: GenerateOptions for (const submodule of submodules) { const content = await docs.toMarkdown({ ...options, - submodule: submodule.name, + submoduleFqn: submodule.fqn, allSubmodules: false, - header: { title: `\`${submodule.name}\` Submodule`, id: submodule.fqn }, + header: { title: `\`${submoduleRelName(submodule)}\` Submodule`, id: submodule.fqn }, }); - await fs.writeFile(path.join(outputPath, `${submodule.name}.${submoduleSuffix}`), content.render()); + await fs.writeFile(path.join(outputPath, `${submoduleRelName(submodule)}.${submoduleSuffix}`), content.render()); } await fs.writeFile(`${outputFileName}.${fileSuffix}`, await (await docs.toIndexMarkdown(submoduleSuffix, options)).render()); @@ -102,3 +102,4 @@ main().catch(e => { console.error(e); process.exit(1); }); + diff --git a/src/docgen/render/markdown-render.ts b/src/docgen/render/markdown-render.ts index dfcd8c74..e560a184 100644 --- a/src/docgen/render/markdown-render.ts +++ b/src/docgen/render/markdown-render.ts @@ -1,7 +1,7 @@ import * as reflect from 'jsii-reflect'; import { MarkdownDocument } from './markdown-doc'; import { ApiReferenceSchema, AssemblyMetadataSchema, ClassSchema, ConstructSchema, EnumMemberSchema, EnumSchema, InitializerSchema, InterfaceSchema, JsiiEntity, MethodSchema, ParameterSchema, PropertySchema, Schema, CURRENT_SCHEMA_VERSION, StructSchema, TypeSchema } from '../schema'; -import { Language } from '../transpile/transpile'; +import { Language, submoduleRelName } from '../transpile/transpile'; export interface MarkdownFormattingOptions { /** @@ -150,7 +150,7 @@ export class MarkdownRenderer { const md = new MarkdownDocument({ header: { title: 'Submodules' }, id: 'submodules' }); md.lines('The following submodules are available:'); for (const submodule of submodules) { - md.lines(`- [${submodule.name}](./${submodule.name}.${fileSuffix})`); + md.lines(`- [${submoduleRelName(submodule)}](./${submoduleRelName(submodule)}.${fileSuffix})`); } return md; } diff --git a/src/docgen/transpile/transpile.ts b/src/docgen/transpile/transpile.ts index 9d8dc4ac..fa2a4a9a 100644 --- a/src/docgen/transpile/transpile.ts +++ b/src/docgen/transpile/transpile.ts @@ -847,7 +847,7 @@ export abstract class TranspileBase implements Transpile { // if the type is in a submodule, the submodule name is the first // part of the namespace. we construct the full submodule fqn and search for it. const submoduleFqn = `${type.assembly.name}.${type.namespace.split('.')[0]}`; - const submodules = type.assembly.submodules.filter( + const submodules = type.assembly.allSubmodules.filter( (s) => s.fqn === submoduleFqn, ); @@ -894,3 +894,12 @@ export abstract class TranspileBase implements Transpile { return 0; } } + +/** + * Return the root-relative name for a submodule + * + * Ex: for a submodule `asm.sub1.sub2`, return `sub1.sub2`. + */ +export function submoduleRelName(submodule: reflect.Submodule) { + return submodule.fqn.split('.').slice(1).join('.'); +} diff --git a/src/docgen/view/api-reference.ts b/src/docgen/view/api-reference.ts index 4c5f7280..6e2f0600 100644 --- a/src/docgen/view/api-reference.ts +++ b/src/docgen/view/api-reference.ts @@ -26,9 +26,9 @@ export class ApiReference { let interfaces: reflect.InterfaceType[]; let enums: reflect.EnumType[]; if (allSubmodules ?? false) { - classes = this.sortByName([...assembly.classes, ...flatMap(assembly.submodules, submod => [...submod.classes])]); - interfaces = this.sortByName([...assembly.interfaces, ...flatMap(assembly.submodules, submod => [...submod.interfaces])]); - enums = this.sortByName([...assembly.enums, ...flatMap(assembly.submodules, submod => [...submod.enums])]); + classes = this.sortByName([...assembly.classes, ...flatMap(assembly.allSubmodules, submod => [...submod.classes])]); + interfaces = this.sortByName([...assembly.interfaces, ...flatMap(assembly.allSubmodules, submod => [...submod.interfaces])]); + enums = this.sortByName([...assembly.enums, ...flatMap(assembly.allSubmodules, submod => [...submod.enums])]); } else { classes = this.sortByName(submodule ? submodule.classes : assembly.classes); interfaces = this.sortByName(submodule ? submodule.interfaces : assembly.interfaces); diff --git a/src/docgen/view/documentation.ts b/src/docgen/view/documentation.ts index df770569..87df5fb1 100644 --- a/src/docgen/view/documentation.ts +++ b/src/docgen/view/documentation.ts @@ -50,9 +50,17 @@ export interface RenderOptions extends TransliterationOptions { * Generate documentation only for a specific submodule. * * @default - Documentation is generated for the root module only. + * @deprecated Prefer `submoduleFqn`. */ readonly submodule?: string; + /** + * Generate documentation only for a specific submodule, identified by its FQN + * + * @default - Documentation is generated for the root module only. + */ + readonly submoduleFqn?: string; + /** * Generate a single document with APIs from all assembly submodules * (including the root). @@ -201,7 +209,7 @@ export class Documentation { */ public async listSubmodules() { const tsAssembly = await this.createAssembly(undefined, { loose: true, validate: false }); - return tsAssembly.submodules; + return tsAssembly.allSubmodules; } public async toIndexMarkdown(fileSuffix:string, options: RenderOptions) { @@ -234,7 +242,12 @@ export class Documentation { throw new LanguageNotSupportedError(`Laguage ${language} is not supported for package ${this.assemblyFqn}`); } - if (allSubmodules && options?.submodule) { + if (options?.submoduleFqn && options.submoduleFqn) { + throw new Error('Supply at most one of \'submodule\' and \'submoduleFqn\''); + } + let submoduleStr = options.submoduleFqn ?? options.submodule; + + if (allSubmodules && submoduleStr) { throw new Error('Cannot call toJson with allSubmodules and a specific submodule both selected.'); } @@ -245,7 +258,7 @@ export class Documentation { throw new Error(`Assembly ${this.assemblyFqn} does not have any targets defined`); } - const submodule = options?.submodule ? this.findSubmodule(assembly, options.submodule) : undefined; + const submodule = submoduleStr ? this.findSubmodule(assembly, submoduleStr) : undefined; let readme: MarkdownDocument | undefined; if (options?.readme ?? false) { @@ -312,29 +325,45 @@ export class Documentation { } /** - * Lookup a submodule by a submodule name. To look up a nested submodule, encode it as a - * dot-separated path, e.g., 'top-level-module.nested-module.another-nested-one'. + * Lookup a submodule by a submodule name. + * + * The contract of this function is historically quite confused: the submodule + * name can be either an FQN (`asm.sub1.sub2`) or just a submodule name + * (`sub1` or `sub1.sub2`). + * + * This is sligthly complicated by ambiguity: `asm.asm.package` and + * `asm.package` can both exist, and which one do you mean when you say + * `asm.package`? + * + * We prefer an FQN match if possible (`asm.sub1.sub2`), but will accept a + * root-relative submodule name as well (`sub1.sub2`). */ private findSubmodule(assembly: reflect.Assembly, submodule: string): reflect.Submodule { - type ReflectSubmodules = typeof assembly.submodules; - return recurse(submodule.split('.'), assembly.submodules); - - function recurse(names: string[], submodules: ReflectSubmodules): reflect.Submodule { - const [head, ...tail] = names; - const found = submodules.filter( - (s) => s.name === head, - ); + const fqnSubs = assembly.allSubmodules.filter( + (s) => s.fqn === submodule, + ); + if (fqnSubs.length === 1) { + return fqnSubs[0]; + } - if (found.length === 0) { - throw new Error(`Submodule ${submodule} not found in assembly ${assembly.name}@${assembly.version}`); - } + // Fallback: assembly-relative name + const relSubs = assembly.allSubmodules.filter( + (s) => s.fqn === `${assembly.name}.${submodule}`, + ); + if (relSubs.length === 1) { + console.error(`[WARNING] findSubmodule() is being called with a relative submodule name: '${submodule}'. Prefer the absolute name: '${assembly.name}.${submodule}'`); + return relSubs[0]; + } - if (found.length > 1) { - throw new Error(`Found multiple submodules with name: ${submodule} in assembly ${assembly.name}@${assembly.version}`); - } + if (fqnSubs.length + relSubs.length === 0) { + throw new Error(`Submodule ${submodule} not found in assembly ${assembly.name}@${assembly.version} (neither as '${submodule}' nor as '${assembly.name}.${submodule})`); + } - return tail.length === 0 ? found[0] : recurse(tail, found[0].submodules); + // Almost impossible that this would be true + if (fqnSubs.length > 1) { + throw new Error(`Found multiple submodules with FQN: ${submodule} in assembly ${assembly.name}@${assembly.version}`); } + throw new Error(`Found multiple submodules with relative name: ${submodule} in assembly ${assembly.name}@${assembly.version}`); } private async createAssembly(