diff --git a/packages/@jsii/spec/lib/assembly.ts b/packages/@jsii/spec/lib/assembly.ts index 779fff7d16..6a73bfd91a 100644 --- a/packages/@jsii/spec/lib/assembly.ts +++ b/packages/@jsii/spec/lib/assembly.ts @@ -148,7 +148,19 @@ export interface Assembly extends AssemblyConfiguration, Documentable { /** * Shareable configuration of a jsii Assembly. */ -export interface AssemblyConfiguration { +export interface AssemblyConfiguration extends Targetable { + /** + * Submodules declared in this assembly. + * + * @default none + */ + submodules?: { [fqn: string]: SourceLocatable & Targetable }; +} + +/** + * An entity on which targets may be configured. + */ +export interface Targetable { /** * A map of target name to configuration, which is used when generating * packages for various languages. diff --git a/packages/jsii-calc/test/assembly.jsii b/packages/jsii-calc/test/assembly.jsii index 11f2d2e72e..328b65012d 100644 --- a/packages/jsii-calc/test/assembly.jsii +++ b/packages/jsii-calc/test/assembly.jsii @@ -137,6 +137,62 @@ "url": "https://github.com/aws/jsii.git" }, "schema": "jsii/0.10.0", + "submodules": { + "jsii-calc.DerivedClassHasNoProperties": { + "locationInModule": { + "filename": "lib/compliance.ts", + "line": 309 + } + }, + "jsii-calc.InterfaceInNamespaceIncludesClasses": { + "locationInModule": { + "filename": "lib/compliance.ts", + "line": 1057 + } + }, + "jsii-calc.InterfaceInNamespaceOnlyInterface": { + "locationInModule": { + "filename": "lib/compliance.ts", + "line": 1048 + } + }, + "jsii-calc.composition": { + "locationInModule": { + "filename": "lib/calculator.ts", + "line": 127 + } + }, + "jsii-calc.submodule": { + "locationInModule": { + "filename": "lib/index.ts", + "line": 7 + } + }, + "jsii-calc.submodule.back_references": { + "locationInModule": { + "filename": "lib/submodule/index.ts", + "line": 4 + } + }, + "jsii-calc.submodule.child": { + "locationInModule": { + "filename": "lib/submodule/index.ts", + "line": 1 + } + }, + "jsii-calc.submodule.nested_submodule": { + "locationInModule": { + "filename": "lib/submodule/nested_submodule.ts", + "line": 3 + } + }, + "jsii-calc.submodule.nested_submodule.deeplyNested": { + "locationInModule": { + "filename": "lib/submodule/nested_submodule.ts", + "line": 4 + } + } + }, "targets": { "dotnet": { "iconUrl": "https://sdk-for-net.amazonwebservices.com/images/AWSLogo128x128.png", @@ -12442,5 +12498,5 @@ } }, "version": "0.0.0", - "fingerprint": "XNjL7+hJ5KwFtBCVEz4TZH4c2JtVW/gTPS7UNrw02Cg=" + "fingerprint": "LPI1XbecdL/VPbXVTmbAHHV0ox1k1cAxzrWF/f0rIxI=" } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii index 11f2d2e72e..328b65012d 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii @@ -137,6 +137,62 @@ "url": "https://github.com/aws/jsii.git" }, "schema": "jsii/0.10.0", + "submodules": { + "jsii-calc.DerivedClassHasNoProperties": { + "locationInModule": { + "filename": "lib/compliance.ts", + "line": 309 + } + }, + "jsii-calc.InterfaceInNamespaceIncludesClasses": { + "locationInModule": { + "filename": "lib/compliance.ts", + "line": 1057 + } + }, + "jsii-calc.InterfaceInNamespaceOnlyInterface": { + "locationInModule": { + "filename": "lib/compliance.ts", + "line": 1048 + } + }, + "jsii-calc.composition": { + "locationInModule": { + "filename": "lib/calculator.ts", + "line": 127 + } + }, + "jsii-calc.submodule": { + "locationInModule": { + "filename": "lib/index.ts", + "line": 7 + } + }, + "jsii-calc.submodule.back_references": { + "locationInModule": { + "filename": "lib/submodule/index.ts", + "line": 4 + } + }, + "jsii-calc.submodule.child": { + "locationInModule": { + "filename": "lib/submodule/index.ts", + "line": 1 + } + }, + "jsii-calc.submodule.nested_submodule": { + "locationInModule": { + "filename": "lib/submodule/nested_submodule.ts", + "line": 3 + } + }, + "jsii-calc.submodule.nested_submodule.deeplyNested": { + "locationInModule": { + "filename": "lib/submodule/nested_submodule.ts", + "line": 4 + } + } + }, "targets": { "dotnet": { "iconUrl": "https://sdk-for-net.amazonwebservices.com/images/AWSLogo128x128.png", @@ -12442,5 +12498,5 @@ } }, "version": "0.0.0", - "fingerprint": "XNjL7+hJ5KwFtBCVEz4TZH4c2JtVW/gTPS7UNrw02Cg=" + "fingerprint": "LPI1XbecdL/VPbXVTmbAHHV0ox1k1cAxzrWF/f0rIxI=" } diff --git a/packages/jsii/lib/assembler.ts b/packages/jsii/lib/assembler.ts index b22353feed..099f95b035 100644 --- a/packages/jsii/lib/assembler.ts +++ b/packages/jsii/lib/assembler.ts @@ -35,7 +35,7 @@ export class Assembler implements Emitter { /** Map of Symbol to namespace export Symbol */ private readonly _submoduleMap = new Map(); - private readonly _submodules = new Set(); + private readonly _submodules = new Map(); /** * @param projectInfo information about the package being assembled @@ -153,6 +153,7 @@ export class Assembler implements Emitter { dependencyClosure: noEmptyDict(toDependencyClosure(this.projectInfo.dependencyClosure)), bundled: this.projectInfo.bundleDependencies, types: this._types, + submodules: noEmptyDict(toSubmoduleDeclarations(this._submodules.values())), targets: this.projectInfo.targets, metadata: this.projectInfo.metadata, docs, @@ -345,17 +346,13 @@ export class Assembler implements Emitter { return `unknown.${typeName}`; } - let submodule = this._submoduleMap.get( type.symbol); - let submoduleNs = submodule?.name; - // Submodules can be in submodules themselves, so we crawl up the tree... - while (submodule != null && this._submoduleMap.has(submodule)) { - submodule = this._submoduleMap.get(submodule)!; - submoduleNs = `${submodule.name}.${submoduleNs}`; + const submodule = this._submoduleMap.get(type.symbol); + if (submodule != null) { + const submoduleNs = this._submodules.get(submodule)!.fqnResolutionPrefix; + return `${submoduleNs}.${typeName}`; } - const fqn = submoduleNs != null - ? `${pkg.name}.${submoduleNs}.${typeName}` - : `${pkg.name}.${typeName}`; + const fqn = `${pkg.name}.${typeName}`; if (pkg.name !== this.projectInfo.name && !this._dereference({ fqn }, type.symbol.valueDeclaration)) { this._diagnostic(node, ts.DiagnosticCategory.Error, @@ -376,10 +373,22 @@ export class Assembler implements Emitter { private _registerNamespaces(symbol: ts.Symbol): void { const declaration = symbol.valueDeclaration ?? symbol.declarations[0]; - if (declaration == null || !ts.isNamespaceExport(declaration)) { + if (declaration == null) { // Nothing to do here... return; } + if (ts.isModuleDeclaration(declaration)) { + const { fqn, fqnResolutionPrefix } = qualifiedNameOf.call(this, symbol, true); + + this._submodules.set(symbol, { fqn, fqnResolutionPrefix, locationInModule: this.declarationLocation(declaration) }); + this._addToSubmodule(symbol, symbol); + return; + } + if (!ts.isNamespaceExport(declaration)) { + // Nothing to do here... + return; + } + const moduleSpecifier = declaration.parent.moduleSpecifier; if (moduleSpecifier == null || !ts.isStringLiteral(moduleSpecifier)) { // There is a grammar error here, so we'll let tsc report this for us. @@ -409,9 +418,29 @@ export class Assembler implements Emitter { this._diagnostic(declaration, ts.DiagnosticCategory.Error, `Submodule namespaces must be camelCased or snake_cased. Consider renaming to "${Case.camel(symbol.name)}".`); } - this._submodules.add(symbol); + + const { fqn, fqnResolutionPrefix } = qualifiedNameOf.call(this, symbol); + const targets = undefined; // This will be configurable in the future. + + this._submodules.set(symbol, { fqn, fqnResolutionPrefix, targets, locationInModule: this.declarationLocation(declaration) }); this._addToSubmodule(symbol, sourceModule); } + + function qualifiedNameOf(this: Assembler, sym: ts.Symbol, inlineNamespace = false): { fqn: string, fqnResolutionPrefix: string } { + if (this._submoduleMap.has(sym)) { + const parent = this._submodules.get(this._submoduleMap.get(sym)!)!; + const fqn = `${parent.fqn}.${sym.name}`; + return { + fqn, + fqnResolutionPrefix: inlineNamespace ? parent.fqnResolutionPrefix : fqn, + }; + } + const fqn = `${this.projectInfo.name}.${sym.name}`; + return { + fqn, + fqnResolutionPrefix: inlineNamespace ? this.projectInfo.name : fqn, + }; + } } /** @@ -479,9 +508,8 @@ export class Assembler implements Emitter { this._addToSubmodule(ns, symbol); } } else if (ts.isModuleDeclaration(decl)) { - this._addToSubmodule(ns, symbol); + this._registerNamespaces(symbol); } else if (ts.isNamespaceExport(decl)) { - this._submoduleMap.set(symbol, ns); this._registerNamespaces(symbol); } } @@ -548,7 +576,7 @@ export class Assembler implements Emitter { // Let's quickly verify the declaration does not collide with a submodule. Submodules get case-adjusted for each // target language separately, so names cannot collide with case-variations. - for (const submodule of this._submodules) { + for (const submodule of this._submodules.keys()) { const candidates = Array.from(new Set([ submodule.name, Case.camel(submodule.name), @@ -1566,6 +1594,30 @@ export class Assembler implements Emitter { } } +interface SubmoduleSpec { + /** + * The submodule's fully qualified name. + */ + readonly fqn: string; + + /** + * The submodule's fully qualified name prefix to use when resolving type FQNs. This does not + * include "inline namespace" names as those are already represented in the TypeCheckers' view of + * the type names. + */ + readonly fqnResolutionPrefix: string; + + /** + * The location of the submodule definition in the source. + */ + readonly locationInModule: spec.SourceLocation; + + /** + * Any customized configuration for the currentl submodule. + */ + readonly targets?: spec.AssemblyTargets; +} + function _fingerprint(assembly: spec.Assembly): spec.Assembly { delete assembly.fingerprint; assembly = sortJson(assembly); @@ -1782,8 +1834,8 @@ function* intersect(xs: Set, ys: Set) { } } -function noEmptyDict(xs: {[key: string]: T}): {[key: string]: T} | undefined { - if (Object.keys(xs).length === 0) { return undefined; } +function noEmptyDict(xs: Record | undefined): Record | undefined { + if (xs == null || Object.keys(xs).length === 0) { return undefined; } return xs; } @@ -1791,11 +1843,27 @@ function toDependencyClosure(assemblies: readonly spec.Assembly[]): { [name: str const result: { [name: string]: spec.AssemblyTargets } = {}; for (const assembly of assemblies) { if (!assembly.targets) { continue; } - result[assembly.name] = { targets: assembly.targets }; + result[assembly.name] = { + submodules: assembly.submodules, + targets: assembly.targets, + }; } return result; } +function toSubmoduleDeclarations(submodules: IterableIterator): spec.Assembly['submodules'] { + const result: spec.Assembly['submodules'] = {}; + + for (const submodule of submodules) { + result[submodule.fqn] = { + locationInModule: submodule.locationInModule, + targets: submodule.targets, + }; + } + + return result; +} + /** * Check whether this type is the intrinsic TypeScript "error type" *