Skip to content

Commit

Permalink
feat: avoid duplicating inlined module-/namespace declarations and ho…
Browse files Browse the repository at this point in the history
…ist nested ones. #149
  • Loading branch information
wessberg committed Sep 23, 2021
1 parent 6c4dc7f commit b21b20c
Show file tree
Hide file tree
Showing 15 changed files with 413 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import {TS} from "../../../../../type/ts";

export interface InlineNamespaceModuleBlockOptions {
intentToAddImportDeclaration (importDeclaration: TS.ImportDeclaration): void;
intentToAddModuleDeclaration (moduleDeclaration: TS.ModuleDeclaration): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {preserveMeta} from "../../util/clone-node-with-meta";
import {DeclarationTransformer} from "../../declaration-bundler-options";
import {InlineNamespaceModuleBlockOptions} from "./inline-namespace-module-block-options";

export function inlineNamespaceModuleBlockTransformer({intentToAddImportDeclaration}: InlineNamespaceModuleBlockOptions): DeclarationTransformer {
export function inlineNamespaceModuleBlockTransformer({intentToAddImportDeclaration, intentToAddModuleDeclaration}: InlineNamespaceModuleBlockOptions): DeclarationTransformer {
return options => {
const {typescript, context, sourceFile, pluginOptions, printer} = options;

Expand All @@ -19,6 +19,7 @@ export function inlineNamespaceModuleBlockTransformer({intentToAddImportDeclarat
const visitorOptions = {
...options,
intentToAddImportDeclaration,
intentToAddModuleDeclaration,

childContinuation: <U extends TS.Node>(node: U): U =>
typescript.visitEachChild(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {TS} from "../../../../../../type/ts";
import {InlineNamespaceModuleBlockVisitorOptions} from "../inline-namespace-module-block-visitor-options";
import {cloneNodeWithMeta} from "../../../util/clone-node-with-meta";

export function visitModuleDeclaration(options: InlineNamespaceModuleBlockVisitorOptions<TS.ModuleDeclaration>): undefined {
const {node, intentToAddModuleDeclaration} = options;
intentToAddModuleDeclaration(cloneNodeWithMeta(node, options));

return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import {TS} from "../../../../../../type/ts";
import {InlineNamespaceModuleBlockVisitorOptions} from "../inline-namespace-module-block-visitor-options";
import {visitImportDeclaration} from "./visit-import-declaration";
import {visitExportDeclaration} from "./visit-export-declaration";
import { visitModuleDeclaration } from "./visit-module-declaration";

export function visitNode({node, ...options}: InlineNamespaceModuleBlockVisitorOptions<TS.Node>): TS.Node|undefined {
if (options.typescript.isImportDeclaration(node)) {
return visitImportDeclaration({...options, node});
} else if (options.typescript.isExportDeclaration(node)) {
return visitExportDeclaration({...options, node});
} else if (options.typescript.isModuleDeclaration(node)) {
return visitModuleDeclaration({...options, node});
} else {
// Only consider root-level statements here
return node;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface ModuleMergerVisitorOptions<T extends TS.Node> extends SourceFil

getMatchingSourceFile(moduleSpecifier: string, from: TS.SourceFile): TS.SourceFile | undefined;
shouldPreserveImportedSymbol(importedSymbol: ImportedSymbol): boolean;
getNameForInlinedModuleDeclaration (moduleSpecifier: string): string|undefined;
markModuleDeclarationAsInlined (moduleSpecifier: string, name: string): void;
includeSourceFile(sourceFile: TS.SourceFile, options?: Partial<IncludeSourceFileOptions>): Iterable<TS.Statement>;
prependNodes(...nodes: TS.Node[]): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import {noExportDeclarationTransformer} from "../no-export-declaration-transform
import {shouldDebugMetrics, shouldDebugSourceFile} from "../../../../../util/is-debug/should-debug";
import {logMetrics} from "../../../../../util/logging/log-metrics";
import {logTransformer} from "../../../../../util/logging/log-transformer";
import { getBindingFromLexicalEnvironment } from "../../util/get-binding-from-lexical-environment";

export function moduleMerger(...transformers: DeclarationTransformer[]): DeclarationTransformer {
return options => {
const {typescript, context, factory, sourceFile, pluginOptions, printer, preservedImports} = options;
const {typescript, context, factory, sourceFile, pluginOptions, printer, preservedImports, inlinedModules} = options;

const fullBenchmark = shouldDebugMetrics(pluginOptions.debug, sourceFile) ? logMetrics(`Merging modules`, sourceFile.fileName) : undefined;

Expand Down Expand Up @@ -54,6 +55,15 @@ export function moduleMerger(...transformers: DeclarationTransformer[]): Declara
} as ModuleMergerVisitorOptions<U>)
) as VisitResult<U>,

getNameForInlinedModuleDeclaration (moduleSpecifier: string): string|undefined {
const name = inlinedModules.get(moduleSpecifier);
if (name == null) return undefined;
return getBindingFromLexicalEnvironment(options.lexicalEnvironment, name) ?? name;
},
markModuleDeclarationAsInlined (moduleSpecifier: string, name: string): void {
inlinedModules.set(moduleSpecifier, name);
},

shouldPreserveImportedSymbol(importedSymbol: ImportedSymbol): boolean {
let importedSymbols = preservedImports.get(importedSymbol.moduleSpecifier);
if (importedSymbols == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,51 +127,78 @@ export function visitExportDeclaration(options: ModuleMergerVisitorOptions<TS.Ex
}

// Otherwise, it if is a named NamespaceExport (such as 'export * as Foo from ".."), we can't just lose the module specifier since 'export * as Foo' isn't valid.
// Instead, we must declare the namespace inline and add an ExportDeclaration with a named export for it
// Instead, we must declare the namespace inline and add an ExportDeclaration with a named export for it. The namespace might already *be* inlined however,
// so we can potentially avoid inlining the same namespace multiple times
else if (typescript.isNamespaceExport?.(contResult.exportClause)) {
const importDeclarations: TS.ImportDeclaration[] = [];

// Otherwise, prepend the nodes for the SourceFile in a namespace declaration
const moduleBlock = factory.createModuleBlock([
...options.includeSourceFile(matchingSourceFile, {
allowDuplicate: true,
allowExports: "skip-optional",
lexicalEnvironment: cloneLexicalEnvironment(),
transformers: [
ensureNoDeclareModifierTransformer,
statementMerger({markAsModuleIfNeeded: false}),
inlineNamespaceModuleBlockTransformer({
intentToAddImportDeclaration: importDeclaration => {
importDeclarations.push(importDeclaration);
}
})
]
})
]);

options.prependNodes(
...importDeclarations.map(importDeclaration => preserveParents(importDeclaration, options)),
preserveParents(
factory.createModuleDeclaration(
undefined,
ensureHasDeclareModifier(undefined, factory, typescript),
factory.createIdentifier(contResult.exportClause.name.text),
moduleBlock,
typescript.NodeFlags.Namespace
),
options
),
preserveParents(
factory.createExportDeclaration(
undefined,
undefined,
false,
factory.createNamedExports([factory.createExportSpecifier(undefined, factory.createIdentifier(contResult.exportClause.name.text))]),
undefined
const moduleDeclarations: TS.ModuleDeclaration[] = [];

const existingInlinedModuleDeclarationName = updatedModuleSpecifier ?? moduleSpecifier == null ? undefined : options.getNameForInlinedModuleDeclaration( updatedModuleSpecifier ?? moduleSpecifier);


if (existingInlinedModuleDeclarationName == null) {
// Otherwise, prepend the nodes for the SourceFile in a namespace declaration
const moduleBlock = factory.createModuleBlock([
...options.includeSourceFile(matchingSourceFile, {
allowDuplicate: true,
allowExports: "skip-optional",
lexicalEnvironment: cloneLexicalEnvironment(),
transformers: [
ensureNoDeclareModifierTransformer,
statementMerger({markAsModuleIfNeeded: false}),
inlineNamespaceModuleBlockTransformer({
intentToAddImportDeclaration: importDeclaration => {
importDeclarations.push(importDeclaration);
},
intentToAddModuleDeclaration: moduleDeclaration => {
moduleDeclarations.push(moduleDeclaration);
}
})
]
})
]);

options.prependNodes(
...importDeclarations.map(importDeclaration => preserveParents(importDeclaration, options)),
...moduleDeclarations.map(moduleDeclaration => preserveParents(moduleDeclaration, options)),
preserveParents(
factory.createModuleDeclaration(
undefined,
ensureHasDeclareModifier(undefined, factory, typescript),
factory.createIdentifier(contResult.exportClause.name.text),
moduleBlock,
typescript.NodeFlags.Namespace
),
options
),
options
preserveParents(
factory.createExportDeclaration(
undefined,
undefined,
false,
factory.createNamedExports([factory.createExportSpecifier(undefined, factory.createIdentifier(contResult.exportClause.name.text))]),
undefined
),
options
)
);
options.markModuleDeclarationAsInlined( updatedModuleSpecifier ?? moduleSpecifier!, contResult.exportClause.name.text);
} else {
options.prependNodes(
preserveParents(
factory.createExportDeclaration(
undefined,
undefined,
false,
factory.createNamedExports([
contResult.exportClause.name.text === existingInlinedModuleDeclarationName ? factory.createExportSpecifier(undefined, factory.createIdentifier(contResult.exportClause.name.text)) : factory.createExportSpecifier(factory.createIdentifier(existingInlinedModuleDeclarationName), factory.createIdentifier(contResult.exportClause.name.text))
]),
undefined
),
options
)
)
);
}
}

// Otherwise, preserve the continuation result, but without the ModuleSpecifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function visitImportTypeNode(options: ModuleMergerVisitorOptions<TS.Impor
const innerContent = factory.createIdentifier(namespaceName);

const importDeclarations: TS.ImportDeclaration[] = [];
const moduleDeclarations: TS.ModuleDeclaration[] = [];
const moduleBlock = factory.createModuleBlock([
...options.includeSourceFile(matchingSourceFile, {
allowDuplicate: true,
Expand All @@ -68,6 +69,9 @@ export function visitImportTypeNode(options: ModuleMergerVisitorOptions<TS.Impor
inlineNamespaceModuleBlockTransformer({
intentToAddImportDeclaration: importDeclaration => {
importDeclarations.push(importDeclaration);
},
intentToAddModuleDeclaration: moduleDeclaration => {
moduleDeclarations.push(moduleDeclaration);
}
})
]
Expand All @@ -76,6 +80,7 @@ export function visitImportTypeNode(options: ModuleMergerVisitorOptions<TS.Impor

options.prependNodes(
...importDeclarations.map(importDeclaration => preserveParents(importDeclaration, options)),
...moduleDeclarations.map(moduleDeclaration => preserveParents(moduleDeclaration, options)),
preserveParents(
factory.createModuleDeclaration(
undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,75 @@ import {ensureNoDeclareModifierTransformer} from "../../ensure-no-declare-modifi
import {statementMerger} from "../../statement-merger/statement-merger";
import {preserveParents} from "../../../util/clone-node-with-meta";
import {inlineNamespaceModuleBlockTransformer} from "../../inline-namespace-module-block-transformer/inline-namespace-module-block-transformer";
import { NamespaceImportedSymbol } from "../../track-imports-transformer/track-imports-transformer-visitor-options";

export function visitNamespaceImport(options: ModuleMergerVisitorOptions<TS.NamespaceImport>): VisitResult<TS.NamespaceImport> {
const {node, factory, typescript, payload} = options;
if (payload.moduleSpecifier == null) return options.childContinuation(node, undefined);

const contResult = options.childContinuation(node, undefined);
const importedSymbol = getImportedSymbolFromNamespaceImport(contResult, payload.moduleSpecifier) as NamespaceImportedSymbol;

// If no SourceFile was resolved, preserve the ImportSpecifier as-is, unless it is already included in the chunk
if (payload.matchingSourceFile == null) {
return options.shouldPreserveImportedSymbol(getImportedSymbolFromNamespaceImport(contResult, payload.moduleSpecifier)) ? contResult : undefined;
}
return options.shouldPreserveImportedSymbol(importedSymbol) ? contResult : undefined;
} else if (options.shouldPreserveImportedSymbol(importedSymbol)) {
const existingInlinedModuleDeclarationName = options.getNameForInlinedModuleDeclaration(payload.moduleSpecifier);

const importDeclarations: TS.ImportDeclaration[] = [];
const moduleBlock = factory.createModuleBlock([
...options.includeSourceFile(payload.matchingSourceFile, {
allowDuplicate: true,
allowExports: "skip-optional",
lexicalEnvironment: cloneLexicalEnvironment(),
transformers: [
ensureNoDeclareModifierTransformer,
statementMerger({markAsModuleIfNeeded: false}),
inlineNamespaceModuleBlockTransformer({
intentToAddImportDeclaration: importDeclaration => {
importDeclarations.push(importDeclaration);
}
if (existingInlinedModuleDeclarationName == null) {
const importDeclarations: TS.ImportDeclaration[] = [];
const moduleDeclarations: TS.ModuleDeclaration[] = [];
const moduleBlock = factory.createModuleBlock([
...options.includeSourceFile(payload.matchingSourceFile, {
allowDuplicate: true,
allowExports: "skip-optional",
lexicalEnvironment: cloneLexicalEnvironment(),
transformers: [
ensureNoDeclareModifierTransformer,
statementMerger({markAsModuleIfNeeded: false}),
inlineNamespaceModuleBlockTransformer({
intentToAddImportDeclaration: importDeclaration => {
importDeclarations.push(importDeclaration);
},
intentToAddModuleDeclaration: moduleDeclaration => {
moduleDeclarations.push(moduleDeclaration);
}
})
]
})
]
})
]);
]);

// Otherwise, prepend the nodes for the SourceFile in a namespace declaration
options.prependNodes(
...importDeclarations.map(importDeclaration => preserveParents(importDeclaration, options)),
preserveParents(
factory.createModuleDeclaration(
undefined,
ensureHasDeclareModifier(undefined, factory, typescript),
factory.createIdentifier(contResult.name.text),
moduleBlock,
typescript.NodeFlags.Namespace
),
options
)
);
// Otherwise, prepend the nodes for the SourceFile in a namespace declaration
options.prependNodes(
...importDeclarations.map(importDeclaration => preserveParents(importDeclaration, options)),
...moduleDeclarations.map(moduleDeclaration => preserveParents(moduleDeclaration, options)),
preserveParents(
factory.createModuleDeclaration(
undefined,
ensureHasDeclareModifier(undefined, factory, typescript),
factory.createIdentifier(contResult.name.text),
moduleBlock,
typescript.NodeFlags.Namespace
),
options
)
);
options.markModuleDeclarationAsInlined(payload.moduleSpecifier, contResult.name.text);
} else {
options.prependNodes(
preserveParents(
factory.createImportEqualsDeclaration(
undefined,
undefined,
false,
factory.createIdentifier(contResult.name.text),
factory.createIdentifier(existingInlinedModuleDeclarationName)
),
{typescript}
)
)
}
}

// Don't include the NamespaceImport
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export interface SourceFileBundlerVisitorOptions extends DeclarationBundlerOptio
// Declarations are represented by IDs which are mapped a string, indicating the deconflicted names for them
declarationToDeconflictedBindingMap: Map<number, string>;
preservedImports: Map<string, Set<ImportedSymbol>>;
inlinedModules: Map<string, string>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export function sourceFileBundler(options: DeclarationBundlerOptions, ...transfo
includedSourceFiles: new Set<string>([firstEntrySourceFile.fileName]),
declarationToDeconflictedBindingMap: new Map<number, string>(),
preservedImports: new Map(),
inlinedModules: new Map(),

resolveSourceFile: (fileName, from) => {
for (const file of [fileName, `${fileName}/index`]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function getImportedSymbolFromImportClauseName(clauseName: TS.Identifier,
export function getImportedSymbolFromNamespaceImport(namespaceImport: TS.NamespaceImport, moduleSpecifier: string): ImportedSymbol {
return {
moduleSpecifier,
isDefaultImport: true,
isNamespaceImport: true,
propertyName: namespaceImport.name,
name: namespaceImport.name
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import {ImportedSymbol} from "../transformers/track-imports-transformer/track-imports-transformer-visitor-options";

export function findMatchingImportedSymbol(importedSymbol: ImportedSymbol, importedSymbols: Iterable<ImportedSymbol>): ImportedSymbol | undefined {
export interface FindMatchingImportedSymbolOptions {
loose: boolean;
}

export function findMatchingImportedSymbol(
importedSymbol: ImportedSymbol,
importedSymbols: Iterable<ImportedSymbol>,
{loose = false}: Partial<FindMatchingImportedSymbolOptions> = {}
): ImportedSymbol | undefined {
for (const otherImportedSymbol of importedSymbols) {
// They both need to point to the same moduleSpecifier
if (importedSymbol.moduleSpecifier !== otherImportedSymbol.moduleSpecifier) continue;

// If it is a NamespaceImport, a matching ImportedSymbol must have the same name
if ("isNamespaceImport" in importedSymbol) {
if ("isNamespaceImport" in otherImportedSymbol && importedSymbol.name.text === otherImportedSymbol.name.text) {
if ("isNamespaceImport" in otherImportedSymbol && (loose || importedSymbol.name.text === otherImportedSymbol.name.text)) {
return otherImportedSymbol;
}
} else if ("isClauseLessImport" in importedSymbol) {
Expand All @@ -21,8 +29,7 @@ export function findMatchingImportedSymbol(importedSymbol: ImportedSymbol, impor
if (
"isDefaultImport" in otherImportedSymbol &&
importedSymbol.isDefaultImport === otherImportedSymbol.isDefaultImport &&
importedSymbol.name.text === otherImportedSymbol.name.text &&
importedSymbol.propertyName.text === otherImportedSymbol.propertyName.text
(loose || (importedSymbol.name.text === otherImportedSymbol.name.text && importedSymbol.propertyName.text === otherImportedSymbol.propertyName.text))
) {
return otherImportedSymbol;
}
Expand Down
Loading

0 comments on commit b21b20c

Please sign in to comment.