Skip to content

Commit

Permalink
fix(namespace-exports): fix problem where ExportDeclarations- and/or …
Browse files Browse the repository at this point in the history
…ImportDeclarations are sometimes lost when inlining ModuleDeclarations. Closes #130
  • Loading branch information
wessberg committed Apr 13, 2021
1 parent 59e31f7 commit deec007
Show file tree
Hide file tree
Showing 18 changed files with 549 additions and 154 deletions.
@@ -0,0 +1,5 @@
import {TS} from "../../../../../type/ts";

export interface InlineNamespaceModuleBlockOptions {
intentToAddImportDeclaration (importDeclaration: TS.ImportDeclaration): void;
}
@@ -0,0 +1,52 @@
import {TS} from "../../../../../type/ts";
import {visitNode} from "./visitor/visit-node";
import {shouldDebugMetrics, shouldDebugSourceFile} from "../../../../../util/is-debug/should-debug";
import {logMetrics} from "../../../../../util/logging/log-metrics";
import {logTransformer} from "../../../../../util/logging/log-transformer";
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 {
return options => {
const {typescript, context, sourceFile, pluginOptions, printer} = options;

const fullBenchmark = shouldDebugMetrics(pluginOptions.debug, sourceFile) ? logMetrics(`Inlining ModuleBlock to be wrapped in a Namespace`, sourceFile.fileName) : undefined;

const transformationLog = shouldDebugSourceFile(pluginOptions.debug, sourceFile) ? logTransformer("Inlining ModuleBlock to be wrapped in a Namespace", sourceFile, printer) : undefined;

// Prepare some VisitorOptions
const visitorOptions = {
...options,
intentToAddImportDeclaration,

childContinuation: <U extends TS.Node>(node: U): U =>
typescript.visitEachChild(
node,
nextNode =>
visitNode({
...visitorOptions,
node: nextNode
}),
context
),

continuation: <U extends TS.Node>(node: U): U =>
visitNode({
...visitorOptions,
node
}) as U
};

const result = preserveMeta(
typescript.visitEachChild(sourceFile, nextNode => visitorOptions.continuation(nextNode), context),
sourceFile,
options
);

transformationLog?.finish(result);
fullBenchmark?.finish();

return result;
};
}
@@ -0,0 +1,11 @@
import {TS} from "../../../../../type/ts";
import {SourceFileBundlerVisitorOptions} from "../source-file-bundler/source-file-bundler-visitor-options";
import {InlineNamespaceModuleBlockOptions} from "./inline-namespace-module-block-options";

export interface InlineNamespaceModuleBlockVisitorOptions<T extends TS.Node> extends SourceFileBundlerVisitorOptions, InlineNamespaceModuleBlockOptions {
typescript: typeof TS;
node: T;

childContinuation<U extends TS.Node>(node: U): U;
continuation<U extends TS.Node>(node: U): U;
}
@@ -0,0 +1,82 @@
import {TS} from "../../../../../../type/ts";
import {InlineNamespaceModuleBlockVisitorOptions} from "../inline-namespace-module-block-visitor-options";
import {preserveParents} from "../../../util/clone-node-with-meta";
import {isNodeFactory} from "../../../util/is-node-factory";
import {generateIdentifierName} from "../../../util/generate-identifier-name";
import {addBindingToLexicalEnvironment} from "../../../util/add-binding-to-lexical-environment";
import {generateUniqueBinding} from "../../../util/generate-unique-binding";
import {isIdentifierFree} from "../../../util/is-identifier-free";
import {getOriginalSourceFile} from "../../../util/get-original-source-file";

export function visitExportDeclaration(options: InlineNamespaceModuleBlockVisitorOptions<TS.ExportDeclaration>): TS.ExportDeclaration|undefined {
const {node, typescript, compatFactory, host, lexicalEnvironment, sourceFile, intentToAddImportDeclaration} = options;

if (node.moduleSpecifier == null || !typescript.isStringLiteralLike(node.moduleSpecifier)) {
return node;
}

// Otherwise, we'll have to generate an ImportDeclaration outside the ModuleBlock and reference it here

if (node.exportClause == null || typescript.isNamespaceExport?.(node.exportClause)) {
const bindingName = generateIdentifierName(node.moduleSpecifier.text, "namespace");
addBindingToLexicalEnvironment(lexicalEnvironment, sourceFile.fileName, bindingName);

const resolveResult = host.resolve(node.moduleSpecifier.text, sourceFile.fileName);
const resolvedFileName = resolveResult?.resolvedAmbientFileName ?? resolveResult?.resolvedFileName;
if (resolvedFileName == null) {
return undefined;
}
const resolvedSourceFile = options.host.getSourceFile(resolvedFileName);
if (resolvedSourceFile == null) {
return undefined;
}
const originalSourceFile = getOriginalSourceFile(node, sourceFile, typescript);

const exportedBindings = [...(resolvedSourceFile as {symbol?: {exports?: Map<string, unknown>}}).symbol?.exports?.keys() ?? []].map(binding => isIdentifierFree(lexicalEnvironment, binding, originalSourceFile.fileName) ? [binding, binding] : [binding, generateUniqueBinding(lexicalEnvironment, binding)]);

const namedImports = compatFactory.createNamedImports(exportedBindings.map(([name, deconflictedName]) =>
compatFactory.createImportSpecifier(
name === deconflictedName ? undefined : compatFactory.createIdentifier(name),
compatFactory.createIdentifier(deconflictedName)
)
));

intentToAddImportDeclaration(
compatFactory.createImportDeclaration(
undefined,
undefined,
isNodeFactory(compatFactory)
? compatFactory.createImportClause(false, undefined, namedImports)
: compatFactory.createImportClause(undefined, namedImports, false),
compatFactory.createStringLiteral(node.moduleSpecifier.text)
)
);

const namedExports = compatFactory.createNamedExports(exportedBindings.map(([name, deconflictedName]) => compatFactory.createExportSpecifier(
name === deconflictedName ? undefined : compatFactory.createIdentifier(deconflictedName), compatFactory.createIdentifier(name)
)));

return preserveParents(
isNodeFactory(compatFactory)
? compatFactory.updateExportDeclaration(
node,
node.decorators,
node.modifiers,
node.isTypeOnly,
namedExports,
undefined
)
: compatFactory.updateExportDeclaration(
node,
node.decorators,
node.modifiers,
namedExports,
undefined,
node.isTypeOnly
),
options
);
}

return node;
}
@@ -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 visitImportDeclaration(options: InlineNamespaceModuleBlockVisitorOptions<TS.ImportDeclaration>): undefined {
const {node, intentToAddImportDeclaration} = options;
intentToAddImportDeclaration(cloneNodeWithMeta(node, options));

return undefined;
}
@@ -0,0 +1,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";

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 {
// Only consider root-level statements here
return node;
}
}
Expand Up @@ -32,7 +32,7 @@ export type ChildVisitResult<T extends TS.Node> = T;

export interface IncludeSourceFileOptions {
allowDuplicate: boolean;
allowExports: boolean;
allowExports: boolean|"skip-optional";
lexicalEnvironment: LexicalEnvironment;
transformers: DeclarationTransformer[];
}
Expand Down
Expand Up @@ -94,14 +94,17 @@ export function moduleMerger(...transformers: DeclarationTransformer[]): Declara
if (options.includedSourceFiles.has(sourceFileToInclude.fileName) && !allowDuplicate) return [];
options.includedSourceFiles.add(sourceFileToInclude.fileName);

const allTransformers = allowExports
const allTransformers = allowExports === true
? [...transformers, ...extraTransformers]
: [
...transformers,
// Removes 'export' modifiers from Nodes
ensureNoExportModifierTransformer,
...(allowExports === false || allowExports === "skip-optional" ? [ensureNoExportModifierTransformer] : []),
// Removes ExportDeclarations and ExportAssignments
noExportDeclarationTransformer,
noExportDeclarationTransformer({
preserveAliasedExports: allowExports === "skip-optional",
preserveExportsWithModuleSpecifiers: allowExports === "skip-optional"
}),
...extraTransformers
];

Expand Down
Expand Up @@ -7,6 +7,7 @@ import {cloneLexicalEnvironment} from "../../../util/clone-lexical-environment";
import {ensureNoDeclareModifierTransformer} from "../../ensure-no-declare-modifier-transformer/ensure-no-declare-modifier-transformer";
import {statementMerger} from "../../statement-merger/statement-merger";
import {isNodeFactory} from "../../../util/is-node-factory";
import {inlineNamespaceModuleBlockTransformer} from "../../inline-namespace-module-block-transformer/inline-namespace-module-block-transformer";

export interface GenerateExportDeclarationsOptions extends Omit<ModuleMergerVisitorOptions<TS.ExportDeclaration>, "node"> {}

Expand Down Expand Up @@ -153,22 +154,36 @@ 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 inline the namespace 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
else if (typescript.isNamespaceExport?.(contResult.exportClause)) {
const importDeclarations: TS.ImportDeclaration[] = [];

// Otherwise, prepend the nodes for the SourceFile in a namespace declaration
const moduleBlock = compatFactory.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(
compatFactory.createModuleDeclaration(
undefined,
ensureHasDeclareModifier(undefined, compatFactory, typescript),
compatFactory.createIdentifier(contResult.exportClause.name.text),
compatFactory.createModuleBlock([
...options.includeSourceFile(matchingSourceFile, {
allowDuplicate: true,
lexicalEnvironment: cloneLexicalEnvironment(),
transformers: [ensureNoDeclareModifierTransformer, statementMerger({markAsModuleIfNeeded: false})]
})
]),
moduleBlock,
typescript.NodeFlags.Namespace
),
options
Expand Down
Expand Up @@ -21,26 +21,27 @@ export function visitExportSpecifier(options: ModuleMergerVisitorOptions<TS.Expo
// Now, we might be referencing the default export from the original module, in which case this should be rewritten to point to the exact identifier
const propertyName = contResult.propertyName ?? contResult.name;

const exportedSymbol =
const namedExportedSymbol =
propertyName.text === "default"
? locateExportedSymbolForSourceFile({defaultExport: true}, {...options, sourceFile: payload.matchingSourceFile.fileName})
: locateExportedSymbolForSourceFile({defaultExport: false, name: propertyName.text}, {...options, sourceFile: payload.matchingSourceFile.fileName});
: locateExportedSymbolForSourceFile({defaultExport: false, name: propertyName.text}, {...options, sourceFile: payload.matchingSourceFile.fileName}) ?? locateExportedSymbolForSourceFile({namespaceExport: true}, {...options, sourceFile: payload.matchingSourceFile.fileName});

if (exportedSymbol != null) {
if (namedExportedSymbol != null) {
// If the export exports a binding from another module *that points to a file that isn't part of the current chunk*,
// Create a new ExportDeclaration that refers to that chunk or external module
const generatedModuleSpecifier =
exportedSymbol.moduleSpecifier == null
namedExportedSymbol.moduleSpecifier == null
? undefined
: generateModuleSpecifier({
...options,
from: payload.matchingSourceFile.fileName,
moduleSpecifier: exportedSymbol.moduleSpecifier
moduleSpecifier: namedExportedSymbol.moduleSpecifier
});

if (
exportedSymbol.moduleSpecifier != null &&
namedExportedSymbol.moduleSpecifier != null &&
generatedModuleSpecifier != null &&
options.getMatchingSourceFile(exportedSymbol.moduleSpecifier, payload.matchingSourceFile) == null
options.getMatchingSourceFile(namedExportedSymbol.moduleSpecifier, payload.matchingSourceFile) == null
) {
options.prependNodes(
preserveParents(
Expand All @@ -53,9 +54,9 @@ export function visitExportSpecifier(options: ModuleMergerVisitorOptions<TS.Expo
compatFactory.createExportSpecifier(
propertyName.text === "default"
? compatFactory.createIdentifier("default")
: exportedSymbol.propertyName.text === contResult.name.text
: !("propertyName" in namedExportedSymbol) || namedExportedSymbol.propertyName == null || namedExportedSymbol.propertyName.text === contResult.name.text
? undefined
: compatFactory.createIdentifier(exportedSymbol.propertyName.text),
: compatFactory.createIdentifier(namedExportedSymbol.propertyName.text),
compatFactory.createIdentifier(contResult.name.text)
)
]),
Expand All @@ -68,9 +69,9 @@ export function visitExportSpecifier(options: ModuleMergerVisitorOptions<TS.Expo
compatFactory.createExportSpecifier(
propertyName.text === "default"
? compatFactory.createIdentifier("default")
: exportedSymbol.propertyName.text === contResult.name.text
: !("propertyName" in namedExportedSymbol) || namedExportedSymbol.propertyName == null || namedExportedSymbol.propertyName.text === contResult.name.text
? undefined
: compatFactory.createIdentifier(exportedSymbol.propertyName.text),
: compatFactory.createIdentifier(namedExportedSymbol.propertyName.text),
compatFactory.createIdentifier(contResult.name.text)
)
]),
Expand All @@ -85,7 +86,7 @@ export function visitExportSpecifier(options: ModuleMergerVisitorOptions<TS.Expo
return preserveMeta(
compatFactory.updateExportSpecifier(
contResult,
exportedSymbol.propertyName.text === contResult.name.text ? undefined : compatFactory.createIdentifier(exportedSymbol.propertyName.text),
!("propertyName" in namedExportedSymbol) || namedExportedSymbol.propertyName == null || namedExportedSymbol.propertyName.text === contResult.name.text ? undefined : compatFactory.createIdentifier(namedExportedSymbol.propertyName.text),
compatFactory.createIdentifier(contResult.name.text)
),
contResult,
Expand Down
Expand Up @@ -8,6 +8,7 @@ import {generateModuleSpecifier} from "../../../util/generate-module-specifier";
import {preserveMeta, preserveParents, preserveSymbols} from "../../../util/clone-node-with-meta";
import {statementMerger} from "../../statement-merger/statement-merger";
import {getParentNode, setParentNode} from "../../../util/get-parent-node";
import {inlineNamespaceModuleBlockTransformer} from "../../inline-namespace-module-block-transformer/inline-namespace-module-block-transformer";

export function visitImportTypeNode(options: ModuleMergerVisitorOptions<TS.ImportTypeNode>): VisitResult<TS.ImportTypeNode> {
const {node, compatFactory, typescript} = options;
Expand Down Expand Up @@ -55,19 +56,32 @@ export function visitImportTypeNode(options: ModuleMergerVisitorOptions<TS.Impor
const namespaceName = generateIdentifierName(matchingSourceFile.fileName, "namespace");
const innerContent = compatFactory.createIdentifier(namespaceName);

const importDeclarations: TS.ImportDeclaration[] = [];
const moduleBlock = compatFactory.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(
compatFactory.createModuleDeclaration(
undefined,
ensureHasDeclareModifier(undefined, compatFactory, typescript),
compatFactory.createIdentifier(namespaceName),
compatFactory.createModuleBlock([
...options.includeSourceFile(matchingSourceFile, {
allowDuplicate: true,
lexicalEnvironment: cloneLexicalEnvironment(),
transformers: [ensureNoDeclareModifierTransformer, statementMerger({markAsModuleIfNeeded: false})]
})
]),
moduleBlock,
typescript.NodeFlags.Namespace
),
options
Expand Down

0 comments on commit deec007

Please sign in to comment.