packages/jsii-reflect/lib/assembly.ts (236 lines of code) (raw):

import * as jsii from '@jsii/spec'; import { ClassType } from './class'; import { Dependency } from './dependency'; import { EnumType } from './enum'; import { InterfaceType } from './interface'; import { ModuleLike } from './module-like'; import { Submodule } from './submodule'; import { Type } from './type'; import { TypeSystem } from './type-system'; export class Assembly extends ModuleLike { private _typeCache?: Map<string, Type>; private _submoduleCache?: Map<string, Submodule>; private _dependencyCache?: Map<string, Dependency>; public constructor( system: TypeSystem, public readonly spec: jsii.Assembly, ) { super(system); } public get fqn(): string { return this.spec.name; } /** * The version of the spec schema */ public get schema(): jsii.SchemaVersion { return this.spec.schema; } /** * The version of the jsii compiler that was used to produce this Assembly. */ public get jsiiVersion(): string { return this.spec.jsiiVersion; } /** * The name of the assembly */ public get name(): string { return this.spec.name; } /** * Description of the assembly, maps to "description" from package.json * This is required since some package managers (like Maven) require it. */ public get description(): string { return this.spec.description; } /** * The metadata associated with the assembly, if any. */ public get metadata(): { readonly [key: string]: any } | undefined { return this.spec.metadata; } /** * The url to the project homepage. Maps to "homepage" from package.json. */ public get homepage(): string { return this.spec.homepage; } /** * The module repository, maps to "repository" from package.json * This is required since some package managers (like Maven) require it. */ public get repository(): { readonly type: string; readonly url: string; readonly directory?: string; } { return this.spec.repository; } /** * The main author of this package. */ public get author(): jsii.Person { return this.spec.author; } /** * Additional contributors to this package. */ public get contributors(): readonly jsii.Person[] { return this.spec.contributors ?? []; } /** * A fingerprint that can be used to determine if the specification has changed. */ public get fingerprint(): string { return this.spec.fingerprint; } /** * The version of the assembly */ public get version(): string { return this.spec.version; } /** * The SPDX name of the license this assembly is distributed on. */ public get license(): string { return this.spec.license; } /** * A map of target name to configuration, which is used when generating packages for * various languages. */ public get targets() { return this.spec.targets; } /** * Dependencies on other assemblies (with semver), the key is the JSII assembly name. */ public get dependencies(): readonly Dependency[] { return Array.from(this._dependencies.values()); } public findDependency(name: string) { const dep = this._dependencies.get(name); if (!dep) { throw new Error(`Dependency ${name} not found for assembly ${this.name}`); } return dep; } /** * List if bundled dependencies (these are not expected to be jsii assemblies). */ public get bundled(): { readonly [module: string]: string } { return this.spec.bundled ?? {}; } /** * The top-level readme document for this assembly (if any). */ public get readme() { return this.spec.readme; } /** * Return the those submodules nested directly under the assembly */ public get submodules(): readonly Submodule[] { const { submodules } = this._analyzeTypes(); return Array.from(submodules.entries()) .filter(([name, _]) => name.split('.').length === 2) .map(([_, submodule]) => submodule); } /** * Return all submodules, even those transtively nested */ public get allSubmodules(): readonly Submodule[] { const { submodules } = this._analyzeTypes(); return Array.from(submodules.values()); } /** * All types in the assembly and all of its submodules */ public get allTypes(): readonly Type[] { return [...this.types, ...this.allSubmodules.flatMap((s) => s.types)]; } /** * All classes in the assembly and all of its submodules */ public get allClasses(): readonly ClassType[] { return this.allTypes .filter((t) => t instanceof ClassType) .map((t) => t as ClassType); } /** * All interfaces in the assembly and all of its submodules */ public get allInterfaces(): readonly InterfaceType[] { return this.allTypes .filter((t) => t instanceof InterfaceType) .map((t) => t as InterfaceType); } /** * All interfaces in the assembly and all of its submodules */ public get allEnums(): readonly EnumType[] { return this.allTypes .filter((t) => t instanceof EnumType) .map((t) => t as EnumType); } public findType(fqn: string) { const type = this.tryFindType(fqn); if (!type) { throw new Error(`Type '${fqn}' not found in assembly ${this.name}`); } return type; } /** * Validate an assembly after loading * * If the assembly was loaded without validation, call this to validate * it after all. Throws an exception if validation fails. */ public validate() { jsii.validateAssembly(this.spec); } protected get submoduleMap(): ReadonlyMap<string, Submodule> { return this._analyzeTypes().submodules; } /** * All types in the root of the assembly */ protected get typeMap(): ReadonlyMap<string, Type> { return this._analyzeTypes().types; } private get _dependencies() { if (!this._dependencyCache) { this._dependencyCache = new Map(); if (this.spec.dependencies) { for (const name of Object.keys(this.spec.dependencies)) { this._dependencyCache.set( name, new Dependency(this.system, name, this.spec.dependencies[name]), ); } } } return this._dependencyCache; } private _analyzeTypes() { if (!this._typeCache || !this._submoduleCache) { this._typeCache = new Map(); const submoduleBuilders = this.discoverSubmodules(); const ts = this.spec.types ?? {}; for (const [fqn, typeSpec] of Object.entries(ts)) { let type: Type; switch (typeSpec.kind) { case jsii.TypeKind.Class: type = new ClassType(this.system, this, typeSpec); break; case jsii.TypeKind.Interface: type = new InterfaceType(this.system, this, typeSpec); break; case jsii.TypeKind.Enum: type = new EnumType(this.system, this, typeSpec); break; default: 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 moduleName = `${this.spec.name}.${submodule}`; submoduleBuilders.get(moduleName)!.addType(type); } else { this._typeCache.set(fqn, type); } } 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(): Map<string, SubmoduleBuilder> { const system = this.system; const ret = new Map<string, SubmoduleBuilder>(); for (const [submoduleName, submoduleSpec] of Object.entries( this.spec.submodules ?? {}, )) { ret.set( submoduleName, new SubmoduleBuilder(system, submoduleSpec, submoduleName, ret), ); } return ret; } } /** * 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 Map<string, Type>(); private _built?: Submodule; public constructor( private readonly system: TypeSystem, private readonly spec: jsii.Submodule, private readonly fullName: string, private readonly allModuleBuilders: Map<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 { this._built ??= new Submodule( this.system, this.spec, this.fullName, mapValues(this.findSubmoduleBuilders(), (b) => b.build()), this.types, ); return this._built; } /** * Return all the builders from the map that are nested underneath ourselves. */ private findSubmoduleBuilders() { const ret = new Map<string, SubmoduleBuilder>(); for (const [k, child] of this.allModuleBuilders) { if (child.isChildOf(this)) { ret.set(k, child); } } return ret; } public addType(type: Type) { this.types.set(type.fqn, type); } } function mapValues<A, B>( xs: ReadonlyMap<string, A>, fn: (x: A) => B, ): Map<string, B> { const ret = new Map<string, B>(); for (const [k, v] of xs) { ret.set(k, fn(v)); } return ret; }