Skip to content

Commit

Permalink
feat: handle nested submodules
Browse files Browse the repository at this point in the history
(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.
  • Loading branch information
rix0rrr committed Nov 21, 2023
1 parent a458dfb commit 5cdb7e0
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 30 deletions.
9 changes: 5 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -102,3 +102,4 @@ main().catch(e => {
console.error(e);
process.exit(1);
});

4 changes: 2 additions & 2 deletions src/docgen/render/markdown-render.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -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;
}
Expand Down
11 changes: 10 additions & 1 deletion src/docgen/transpile/transpile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);

Expand Down Expand Up @@ -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('.');
}
6 changes: 3 additions & 3 deletions src/docgen/view/api-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
69 changes: 49 additions & 20 deletions src/docgen/view/documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.');
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 5cdb7e0

Please sign in to comment.