Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(jsii): dependency submodules are not tagged #1663

Merged
merged 3 commits into from
May 14, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions packages/jsii/lib/assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class Assembler implements Emitter {
const symbol = this._typeChecker.getSymbolAtLocation(sourceFile);
if (symbol) {
const moduleExports = this._typeChecker.getExportsOfModule(symbol);
await Promise.all(moduleExports.map(this._registerNamespaces.bind(this)));
await Promise.all(moduleExports.map(item => this._registerNamespaces(item, this.projectInfo.projectRoot)));
for (const node of moduleExports) {
visitPromises.push(this._visitNode(node.declarations[0], new EmitContext([], this.projectInfo.stability)));
}
Expand Down Expand Up @@ -377,14 +377,24 @@ export class Assembler implements Emitter {
// It's unlikely, but if we can't get the SourceFile here, ignore it (TypeScript compilation probably failed)
if (depMod == null) { continue; }

const depRoot = packageRoot(resolved.resolvedModule.resolvedFileName);

for (const symbol of this._typeChecker.getExportsOfModule(depMod)) {
// eslint-disable-next-line no-await-in-loop
await this._registerNamespaces(symbol);
await this._registerNamespaces(symbol, depRoot);
}
}

function packageRoot(file: string): string {
const parent = path.dirname(file);
if (path.basename(parent) === 'node_modules' || parent === file) {
return file;
}
return packageRoot(parent);
}
}

private async _registerNamespaces(symbol: ts.Symbol): Promise<void> {
private async _registerNamespaces(symbol: ts.Symbol, packageRoot: string): Promise<void> {
const declaration = symbol.valueDeclaration ?? symbol.declarations[0];
if (declaration == null) {
// Nothing to do here...
Expand All @@ -394,7 +404,7 @@ export class Assembler implements Emitter {
const { fqn, fqnResolutionPrefix } = await qualifiedNameOf.call(this, symbol, true);

this._submodules.set(symbol, { fqn, fqnResolutionPrefix, locationInModule: this.declarationLocation(declaration) });
await this._addToSubmodule(symbol, symbol);
await this._addToSubmodule(symbol, symbol, packageRoot);
return;
}
if (!ts.isNamespaceExport(declaration)) {
Expand All @@ -417,12 +427,21 @@ export class Assembler implements Emitter {
// Unresolvable module... We'll let tsc report this for us.
return;
}
if (resolution.resolvedModule.isExternalLibraryImport) {
if (
// We're not looking into a dependency's namespace exports, and the resolution says it's external
(packageRoot === this.projectInfo.projectRoot && resolution.resolvedModule.isExternalLibraryImport)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this two conditions ever happen together?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. They'll do in the phase where we're actually compiling our project (as opposed to crawling dependencies).

// Or the module resolves outside of the current dependency's tree entirely
|| !resolution.resolvedModule.resolvedFileName.startsWith(packageRoot)
// Or the module is under one the current dependency's node_modules subtree
|| resolution.resolvedModule.resolvedFileName.split(path.sep).filter(entry => entry === 'node_modules').length
!== packageRoot.split(path.sep).filter(entry => entry === 'node_modules').length
) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help me out here, why does a different number of node_modules folder in a path mean we don't want to register it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we reach here, we've already established that the file resolved within the same root (foo/node_modules/<dependency>/<some_file>). This means the file belongs do <dependency>, unless it happens to be a dependency that wasn't de-duplicated up (foo/node_modules/<dependency>/node_modules/<dependency-of-dependency>), at which point this is effectively a re-export of another module, and we don't want to duplicate those types (it'd be SUPER awkward in Java & C# at the very least)

// External re-exports are "pure-javascript" sugar; they need not be
// represented in the jsii Assembly since the types in there will be
// resolved through dependencies.
return;
}
Comment on lines +431 to 443
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To give me more context, is this if an optimization? If not, what are we preventing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are preventing creating a second name for the same actual type, because while TypeScript will totally follow the re-export and understand the type is just being aliased; other languages that do not feature structural type-checking will consider the two instances of the type as entirely distinct entities - that is no bueno.


const sourceFile = this.program.getSourceFile(resolution.resolvedModule.resolvedFileName)!;
const sourceModule = this._typeChecker.getSymbolAtLocation(sourceFile);
// If there's no module, it's a syntax error, and tsc will have reported it for us.
Expand All @@ -436,7 +455,7 @@ export class Assembler implements Emitter {
const targets = undefined; // This will be configurable in the future.

this._submodules.set(symbol, { fqn, fqnResolutionPrefix, targets, locationInModule: this.declarationLocation(declaration) });
await this._addToSubmodule(symbol, sourceModule);
await this._addToSubmodule(symbol, sourceModule, packageRoot);
}

async function qualifiedNameOf(this: Assembler, sym: ts.Symbol, inlineNamespace = false): Promise<{ fqn: string, fqnResolutionPrefix: string }> {
Expand Down Expand Up @@ -464,10 +483,11 @@ export class Assembler implements Emitter {
* declarations exported by an `export * as ns from 'moduleLike';` statement
* so that they can subsequently be correctly namespaced.
*
* @param ns the symbol that identifies the submodule.
* @param moduleLike the module-like symbol bound to the submodule.
* @param ns the symbol that identifies the submodule.
* @param moduleLike the module-like symbol bound to the submodule.
* @param packageRoot the root of the package being traversed.
*/
private async _addToSubmodule(ns: ts.Symbol, moduleLike: ts.Symbol) {
private async _addToSubmodule(ns: ts.Symbol, moduleLike: ts.Symbol, packageRoot: string) {
// For each symbol exported by the moduleLike, map it to the ns submodule.
for (const symbol of this._typeChecker.getExportsOfModule(moduleLike)) {
if (this._submoduleMap.has(symbol)) {
Expand Down Expand Up @@ -522,14 +542,14 @@ export class Assembler implements Emitter {
}
if (type.symbol.exports) {
// eslint-disable-next-line no-await-in-loop
await this._addToSubmodule(ns, symbol);
await this._addToSubmodule(ns, symbol, packageRoot);
}
} else if (ts.isModuleDeclaration(decl)) {
// eslint-disable-next-line no-await-in-loop
await this._registerNamespaces(symbol);
await this._registerNamespaces(symbol, packageRoot);
} else if (ts.isNamespaceExport(decl)) {
// eslint-disable-next-line no-await-in-loop
await this._registerNamespaces(symbol);
await this._registerNamespaces(symbol, packageRoot);
}
}
}
Expand Down