Skip to content

Commit

Permalink
feat: submodules expose readmes and targets via jsii-reflect (#2482)
Browse files Browse the repository at this point in the history
For the documentation for monocdk/v2, individual submodules need to be
able to have `README`s attached to them, as well as the `targets`
configuration of submodules needs to be exposed so that we can use this
to determine the FQNs of types in other languages (Java/C#/...).



---

By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license].

[Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
  • Loading branch information
rix0rrr authored Jan 27, 2021
1 parent 2743d52 commit 33f41eb
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 65 deletions.
39 changes: 24 additions & 15 deletions packages/@jsii/spec/lib/assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,6 @@ export interface Assembly extends AssemblyConfiguration, Documentable {
*/
types?: { [fqn: string]: Type };

/**
* The top-level readme document for this assembly (if any).
*
* @default none
*/
readme?: { markdown: string };

/**
* List of bin-scripts
*
Expand All @@ -161,30 +154,46 @@ export interface AssemblyConfiguration extends Targetable {
*
* @default none
*/
submodules?: { [fqn: string]: SourceLocatable & Targetable };
submodules?: { [fqn: string]: Submodule };
}

/**
* An entity on which targets may be configured.
* A targetable module-like thing
*
* Has targets and a readme. Used for Assemblies and Submodules.
*/
export interface Targetable {
/**
* Submodules defined in this assembly, if any, associated with their
* designated targets configuration.
* A map of target name to configuration, which is used when generating
* packages for various languages.
*
* @default none
*/
submodules?: { [fqn: string]: { targets?: AssemblyTargets } };
targets?: AssemblyTargets;

/**
* A map of target name to configuration, which is used when generating
* packages for various languages.
* The readme document for this module (if any).
*
* @default none
*/
targets?: AssemblyTargets;
readme?: ReadMe;
}

/**
* README information
*/
export interface ReadMe {
markdown: string;
}

/**
* A submodule
*
* The difference between a top-level module (the assembly) and a submodule is
* that the submodule is annotated with its location in the repository.
*/
export type Submodule = SourceLocatable & Targetable;

/**
* Versions of the JSII Assembly Specification.
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/jsii-calc/lib/submodule/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Read you, read me
=================

This is the readme of the `jsii-calc.submodule` module.
4 changes: 4 additions & 0 deletions packages/jsii-calc/lib/submodule/isolated.README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Read you, read me
=================

This is the readme of the `jsii-calc.submodule.isolated` module.
8 changes: 7 additions & 1 deletion packages/jsii-calc/test/assembly.jsii
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@
"locationInModule": {
"filename": "lib/index.ts",
"line": 10
},
"readme": {
"markdown": "Read you, read me\n=================\n\nThis is the readme of the `jsii-calc.submodule` module.\n"
}
},
"jsii-calc.submodule.back_references": {
Expand All @@ -229,6 +232,9 @@
"locationInModule": {
"filename": "lib/submodule/index.ts",
"line": 2
},
"readme": {
"markdown": "Read you, read me\n=================\n\nThis is the readme of the `jsii-calc.submodule.isolated` module.\n"
}
},
"jsii-calc.submodule.nested_submodule": {
Expand Down Expand Up @@ -14435,5 +14441,5 @@
}
},
"version": "0.0.0",
"fingerprint": "2KFlnOkWR5oOwLlri+7JlkA0BsYvnRkpBrk4nPWnJfw="
"fingerprint": "KsLuN2pR5TnEIZgswnzHPCnKNicXnyS8nAh2JPJIVrY="
}
154 changes: 110 additions & 44 deletions packages/jsii-reflect/lib/assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,20 @@ export class Assembly extends ModuleLike {
return this.spec.readme;
}

/**
* Return the those submodules nested directly under the assembly
*/
public get submodules(): readonly Submodule[] {
const { submodules } = this._types;
return Object.entries(submodules)
.filter(([name, _]) => name.split('.').length === 2)
.map(([_, submodule]) => submodule);
}

/**
* Return all submodules, even those transtively nested
*/
public get allSubmodules(): readonly Submodule[] {
const { submodules } = this._types;
return Object.values(submodules);
}
Expand Down Expand Up @@ -203,17 +216,10 @@ export class Assembly extends ModuleLike {
if (!this._typeCache || !this._submoduleCache) {
this._typeCache = {};

const submodules: { [fullName: string]: SubmoduleMap } = {};
const submoduleBuilders = this.discoverSubmodules();

const ts = this.spec.types ?? {};
for (const fqn of Object.keys(ts)) {
const typeSpec = ts[fqn];

let submodule = typeSpec.namespace;
while (submodule != null && `${this.spec.name}.${submodule}` in ts) {
submodule = ts[`${this.spec.name}.${submodule}`].namespace;
}

for (const [fqn, typeSpec] of Object.entries(ts)) {
let type: Type;
switch (typeSpec.kind) {
case jsii.TypeKind.Class:
Expand All @@ -232,53 +238,113 @@ export class Assembly extends ModuleLike {
throw new Error('Unknown type kind');
}

// Find containing submodule (potentially through containing nested classes,
// which DO count as namespaces but don't count as modules)
let submodule = typeSpec.namespace;
while (submodule != null && `${this.spec.name}.${submodule}` in ts) {
submodule = ts[`${this.spec.name}.${submodule}`].namespace;
}

if (submodule != null) {
const [root, ...parts] = submodule.split('.');
let container = (submodules[root] = submodules[root] ?? {
submodules: {},
types: [],
});
for (const part of parts) {
container = container.submodules[part] = container.submodules[
part
] ?? { submodules: {}, types: [] };
}
container.types.push(type);
const moduleName = `${this.spec.name}.${submodule}`;
submoduleBuilders[moduleName].addType(type);
} else {
this._typeCache[fqn] = type;
}
}

this._submoduleCache = {};
for (const [name, map] of Object.entries(submodules)) {
this._submoduleCache[name] = makeSubmodule(
this.system,
map,
`${this.name}.${name}`,
);
}
this._submoduleCache = mapValues(submoduleBuilders, (b) => b.build());
}

return { types: this._typeCache, submodules: this._submoduleCache };
}

/**
* Return a builder for all submodules in this assembly (so that we can
* add types into the objects).
*/
private discoverSubmodules(): Record<string, SubmoduleBuilder> {
const system = this.system;

const ret: Record<string, SubmoduleBuilder> = {};
for (const [submoduleName, submoduleSpec] of Object.entries(
this.spec.submodules ?? {},
)) {
ret[submoduleName] = new SubmoduleBuilder(
system,
submoduleSpec,
submoduleName,
ret,
);
}
return ret;
}
}

interface SubmoduleMap {
readonly submodules: { [fullName: string]: SubmoduleMap };
readonly types: Type[];
/**
* Mutable Submodule builder
*
* Allows adding Types before the submodule is frozen to a Submodule class.
*
* Takes a reference to the full map of submodule builders, so that come time
* to translate
*/
class SubmoduleBuilder {
private readonly types = new Array<Type>();

private _built?: Submodule;

public constructor(
private readonly system: TypeSystem,
private readonly spec: jsii.Submodule,
private readonly fullName: string,
private readonly allModuleBuilders: Record<string, SubmoduleBuilder>,
) {}

/**
* Whether this submodule is a direct child of another submodule
*/
public isChildOf(other: SubmoduleBuilder) {
return (
this.fullName.startsWith(`${other.fullName}.`) &&
this.fullName.split('.').length === other.fullName.split('.').length + 1
);
}

public build(): Submodule {
if (!this._built) {
this._built = new Submodule(
this.system,
this.spec,
this.fullName,
this.findSubmoduleBuilders().map((b) => b.build()),
this.types,
);
}
return this._built;
}

/**
* Return all the builders from the map that are nested underneath ourselves.
*/
private findSubmoduleBuilders() {
return Object.values(this.allModuleBuilders).filter((child) =>
child.isChildOf(this),
);
}

public addType(type: Type) {
this.types.push(type);
}
}

function makeSubmodule(
system: TypeSystem,
map: SubmoduleMap,
fullName: string,
): Submodule {
return new Submodule(
system,
fullName,
Object.entries(map.submodules).map(([name, subMap]) =>
makeSubmodule(system, subMap, `${fullName}.${name}`),
),
map.types,
);
function mapValues<A, B>(
xs: Record<string, A>,
fn: (x: A) => B,
): Record<string, B> {
const ret: Record<string, B> = {};
for (const [k, v] of Object.entries(xs)) {
ret[k] = fn(v);
}
return ret;
}
21 changes: 19 additions & 2 deletions packages/jsii-reflect/lib/module-like.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as jsii from '@jsii/spec';

import { ClassType } from './class';
import { EnumType } from './enum';
import { InterfaceType } from './interface';
Expand All @@ -7,9 +9,20 @@ import { TypeSystem } from './type-system';

export abstract class ModuleLike {
public declare abstract readonly fqn: string;

/**
* Return direct submodules
*/
public declare abstract readonly submodules: readonly Submodule[];
public declare abstract readonly types: readonly Type[];

/**
* A map of target name to configuration, which is used when generating packages for
* various languages.
*/
public declare abstract readonly targets?: jsii.AssemblyTargets;
public declare abstract readonly readme?: jsii.ReadMe;

protected constructor(public readonly system: TypeSystem) {}

public get classes(): readonly ClassType[] {
Expand Down Expand Up @@ -40,8 +53,12 @@ export abstract class ModuleLike {
return undefined;
}

const [subName] = fqn.slice(this.fqn.length + 1).split('.');
const sub = this.submodules.find((sub) => sub.name === subName);
const myFqnLength = this.fqn.split('.').length;
const subFqn = fqn
.split('.')
.slice(0, myFqnLength + 1)
.join('.');
const sub = this.submodules.find((sub) => sub.fqn === subFqn);
return sub?.tryFindType(fqn);
}
}
18 changes: 18 additions & 0 deletions packages/jsii-reflect/lib/submodule.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as jsii from '@jsii/spec';

import { ModuleLike } from './module-like';
import { Type } from './type';
import { TypeSystem } from './type-system';
Expand All @@ -10,6 +12,7 @@ export class Submodule extends ModuleLike {

public constructor(
system: TypeSystem,
public readonly spec: jsii.Submodule,
public readonly fqn: string,
public readonly submodules: readonly Submodule[],
public readonly types: readonly Type[],
Expand All @@ -18,4 +21,19 @@ export class Submodule extends ModuleLike {

this.name = fqn.split('.').pop()!;
}

/**
* A map of target name to configuration, which is used when generating packages for
* various languages.
*/
public get targets() {
return this.spec.targets;
}

/**
* The top-level readme document for this assembly (if any).
*/
public get readme() {
return this.spec.readme;
}
}
21 changes: 21 additions & 0 deletions packages/jsii-reflect/test/type-system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,27 @@ describe('@deprecated', () => {
});
});

/**
* This test is actually testing the combination of `jsii`, `jsii-calc` and `jsii-reflect`.
*/
test('Submodules can have a README', () => {
const jsiiCalc = typesys.findAssembly('jsii-calc');

const submodule = jsiiCalc.submodules.find(
(m) => m.fqn === 'jsii-calc.submodule',
);
const isolated = submodule?.submodules.find(
(m) => m.fqn === 'jsii-calc.submodule.isolated',
);

expect(submodule?.readme?.markdown).toMatch(
/This is the readme.*jsii-calc.submodule/,
);
expect(isolated?.readme?.markdown).toMatch(
/This is the readme.*jsii-calc.submodule.isolated/,
);
});

test('overridden member knows about both parent types', async () => {
const ts = await typeSystemFromSource(`
export class Foo {
Expand Down
Loading

0 comments on commit 33f41eb

Please sign in to comment.