Skip to content

Commit

Permalink
Handle re-exports of synthetic named exports (#3319)
Browse files Browse the repository at this point in the history
* Fix: handle re-exports of synthetic named exports

* fix systemjs

* reenable all tests

* emit var or const

* fix reexports

* apply feedback

* wip

* Add test case for global name conflict

* Add test case for multi-level synthetic named exports

* Add test case for handling and deduplicating multiple synthetic named and default imports

* Fix esm synthetic export deconflicting

* Fix reeports of synthetic named exports

* Slightly improve coverage

* Fix namespace object generation

Co-authored-by: Lukas Taegert-Atkinson <lukas.taegert-atkinson@tngtech.com>
  • Loading branch information
manucorporat and lukastaegert committed Apr 7, 2020
1 parent d18cb37 commit aecd3e1
Show file tree
Hide file tree
Showing 105 changed files with 908 additions and 89 deletions.
52 changes: 41 additions & 11 deletions src/Chunk.ts
Expand Up @@ -8,6 +8,7 @@ import ExportDefaultVariable from './ast/variables/ExportDefaultVariable';
import ExportShimVariable from './ast/variables/ExportShimVariable';
import LocalVariable from './ast/variables/LocalVariable';
import NamespaceVariable from './ast/variables/NamespaceVariable';
import SyntheticNamedExportVariable from './ast/variables/SyntheticNamedExportVariable';
import Variable from './ast/variables/Variable';
import ExternalModule from './ExternalModule';
import finalisers from './finalisers/index';
Expand Down Expand Up @@ -64,6 +65,7 @@ export type ChunkDependencies = ModuleDeclarationDependency[];

export type ChunkExports = {
exported: string;
expression: string | null;
hoisted: boolean;
local: string;
uninitialized: boolean;
Expand Down Expand Up @@ -317,6 +319,7 @@ export default class Chunk {
safeExportName: string;
this.exportNames = Object.create(null);
this.sortedExportNames = null;

if (mangle) {
for (const variable of this.exports) {
const suggestedName = variable.name[0];
Expand Down Expand Up @@ -507,7 +510,10 @@ export default class Chunk {
this.setExternalRenderPaths(options, inputBase);

this.renderedDependencies = this.getChunkDependencyDeclarations(options);
this.renderedExports = this.exportMode === 'none' ? [] : this.getChunkExportDeclarations();
this.renderedExports =
this.exportMode === 'none'
? []
: this.getChunkExportDeclarations(options.format as InternalModuleFormat);

timeEnd('render modules', 3);
}
Expand Down Expand Up @@ -744,8 +750,8 @@ export default class Chunk {
importName = exportName = '*';
} else {
const variable = this.exportNames[exportName];
if (variable instanceof SyntheticNamedExportVariable) continue;
const module = variable.module;
// skip local exports
if (!module || module.chunk === this) continue;
if (module instanceof Module) {
exportChunk = module.chunk!;
Expand Down Expand Up @@ -830,17 +836,20 @@ export default class Chunk {
return dependencies;
}

private getChunkExportDeclarations(): ChunkExports {
private getChunkExportDeclarations(format: InternalModuleFormat): ChunkExports {
const exports: ChunkExports = [];
for (const exportName of this.getExportNames()) {
if (exportName[0] === '*') continue;

const variable = this.exportNames[exportName];
const module = variable.module;

if (module && module.chunk !== this) continue;
if (!(variable instanceof SyntheticNamedExportVariable)) {
const module = variable.module;
if (module && module.chunk !== this) continue;
}
let expression = null;
let hoisted = false;
let uninitialized = false;
let local = variable.getName();
if (variable instanceof LocalVariable) {
if (variable.init === UNDEFINED_EXPRESSION) {
uninitialized = true;
Expand All @@ -855,12 +864,18 @@ export default class Chunk {
break;
}
}
} else if (variable instanceof SyntheticNamedExportVariable) {
expression = local;
if (format === 'es' && exportName !== 'default') {
local = variable.renderName!;
}
}

exports.push({
exported: exportName,
expression,
hoisted,
local: variable.getName(),
local,
uninitialized
});
}
Expand Down Expand Up @@ -944,6 +959,8 @@ export default class Chunk {
}

private setIdentifierRenderResolutions(options: OutputOptions) {
const syntheticExports = new Set<SyntheticNamedExportVariable>();

for (const exportName of this.getExportNames()) {
const exportVariable = this.exportNames[exportName];
if (exportVariable instanceof ExportShimVariable) {
Expand All @@ -957,6 +974,8 @@ export default class Chunk {
!exportVariable.isId
) {
exportVariable.setRenderNames('exports', exportName);
} else if (exportVariable instanceof SyntheticNamedExportVariable) {
syntheticExports.add(exportVariable);
} else {
exportVariable.setRenderNames(null, null);
}
Expand Down Expand Up @@ -985,13 +1004,17 @@ export default class Chunk {
usedNames,
options.format as string,
options.interop !== false,
this.graph.preserveModules
this.graph.preserveModules,
syntheticExports
);
}

private setUpChunkImportsAndExportsForModule(module: Module) {
for (const variable of module.imports) {
for (let variable of module.imports) {
if ((variable.module as Module).chunk !== this) {
if (variable instanceof SyntheticNamedExportVariable) {
variable = variable.getOriginalVariable();
}
this.imports.add(variable);
if (variable.module instanceof Module) {
variable.module.chunk!.exports.add(variable);
Expand All @@ -1005,9 +1028,16 @@ export default class Chunk {
const map = module.getExportNamesByVariable();
for (const exportedVariable of map.keys()) {
this.exports.add(exportedVariable);
const exportingModule = exportedVariable.module;
const isSynthetic = exportedVariable instanceof SyntheticNamedExportVariable;
const importedVariable = isSynthetic
? (exportedVariable as SyntheticNamedExportVariable).getOriginalVariable()
: exportedVariable;
const exportingModule = importedVariable.module;
if (exportingModule && exportingModule.chunk && exportingModule.chunk !== this) {
exportingModule.chunk.exports.add(exportedVariable);
exportingModule.chunk.exports.add(importedVariable);
if (isSynthetic) {
this.imports.add(importedVariable);
}
}
}
}
Expand Down
30 changes: 22 additions & 8 deletions src/Module.ts
Expand Up @@ -100,7 +100,7 @@ export interface AstContext {
getModuleName: () => string;
getReexports: () => string[];
importDescriptions: { [name: string]: ImportDescription };
includeAndGetReexportedExternalNamespaces: () => ExternalVariable[];
includeAndGetAdditionalMergedNamespaces: (context: InclusionContext) => Variable[];
includeDynamicImport: (node: ImportExpression) => void;
includeVariable: (context: InclusionContext, variable: Variable) => void;
magicString: MagicString;
Expand Down Expand Up @@ -351,11 +351,20 @@ export default class Module {
if (this.relevantDependencies) return this.relevantDependencies;
const relevantDependencies = new Set<Module | ExternalModule>();
for (const variable of this.imports) {
relevantDependencies.add(variable.module!);
relevantDependencies.add(
variable instanceof SyntheticNamedExportVariable
? variable.getOriginalVariable().module!
: variable.module!
);
}
if (this.isEntryPoint || this.dynamicallyImportedBy.length > 0 || this.graph.preserveModules) {
for (const exportName of [...this.getReexports(), ...this.getExports()]) {
relevantDependencies.add(this.getVariableForExportName(exportName).module as Module);
const variable = this.getVariableForExportName(exportName);
relevantDependencies.add(
variable instanceof SyntheticNamedExportVariable
? variable.getOriginalVariable().module!
: variable.module!
);
}
}
if (this.graph.treeshakingOptions) {
Expand Down Expand Up @@ -700,7 +709,7 @@ export default class Module {
getModuleName: this.basename.bind(this),
getReexports: this.getReexports.bind(this),
importDescriptions: this.importDescriptions,
includeAndGetReexportedExternalNamespaces: this.includeAndGetReexportedExternalNamespaces.bind(
includeAndGetAdditionalMergedNamespaces: this.includeAndGetAdditionalMergedNamespaces.bind(
this
),
includeDynamicImport: this.includeDynamicImport.bind(this),
Expand Down Expand Up @@ -896,17 +905,22 @@ export default class Module {
}
}

private includeAndGetReexportedExternalNamespaces(): ExternalVariable[] {
const reexportedExternalNamespaces: ExternalVariable[] = [];
private includeAndGetAdditionalMergedNamespaces(context: InclusionContext): Variable[] {
const mergedNamespaces: Variable[] = [];
for (const module of this.exportAllModules) {
if (module instanceof ExternalModule) {
const externalVariable = module.getVariableForExportName('*');
externalVariable.include();
this.imports.add(externalVariable);
reexportedExternalNamespaces.push(externalVariable);
mergedNamespaces.push(externalVariable);
} else if (module.syntheticNamedExports) {
const syntheticNamespace = module.getDefaultExport();
syntheticNamespace.include(context);
this.imports.add(syntheticNamespace);
mergedNamespaces.push(syntheticNamespace);
}
}
return reexportedExternalNamespaces;
return mergedNamespaces;
}

private includeDynamicImport(node: ImportExpression) {
Expand Down
23 changes: 11 additions & 12 deletions src/ast/variables/NamespaceVariable.ts
Expand Up @@ -4,7 +4,6 @@ import { RESERVED_NAMES } from '../../utils/reservedNames';
import { InclusionContext } from '../ExecutionContext';
import Identifier from '../nodes/Identifier';
import { UNKNOWN_PATH } from '../utils/PathTracker';
import ExternalVariable from './ExternalVariable';
import Variable from './Variable';

export default class NamespaceVariable extends Variable {
Expand All @@ -13,7 +12,7 @@ export default class NamespaceVariable extends Variable {
memberVariables: { [name: string]: Variable } = Object.create(null);
module: Module;

private reexportedExternalNamespaces: ExternalVariable[] = [];
private mergedNamespaces: Variable[] = [];
private referencedEarly = false;
private references: Identifier[] = [];
private syntheticNamedExports: boolean;
Expand Down Expand Up @@ -49,7 +48,7 @@ export default class NamespaceVariable extends Variable {
break;
}
}
this.reexportedExternalNamespaces = this.context.includeAndGetReexportedExternalNamespaces();
this.mergedNamespaces = this.context.includeAndGetAdditionalMergedNamespaces(context);
if (this.context.preserveModules) {
for (const memberName of Object.keys(this.memberVariables))
this.memberVariables[memberName].include(context);
Expand Down Expand Up @@ -91,21 +90,21 @@ export default class NamespaceVariable extends Variable {
members.unshift(`${t}[Symbol.toStringTag]:${_}'Module'`);
}

const hasExternalReexports = this.reexportedExternalNamespaces.length > 0;
if (!hasExternalReexports) members.unshift(`${t}__proto__:${_}null`);
const needsObjectAssign = this.mergedNamespaces.length > 0 || this.syntheticNamedExports;
if (!needsObjectAssign) members.unshift(`${t}__proto__:${_}null`);

let output = `{${n}${members.join(`,${n}`)}${n}}`;
if (hasExternalReexports || this.syntheticNamedExports) {
const assignmentArgs = members.length > 0 ? [output] : [];
if (hasExternalReexports) {
assignmentArgs.unshift(
'/*#__PURE__*/Object.create(null)',
...this.reexportedExternalNamespaces.map(variable => variable.getName())
);
if (needsObjectAssign) {
const assignmentArgs: string[] = ['/*#__PURE__*/Object.create(null)'];
if (this.mergedNamespaces.length > 0) {
assignmentArgs.push(...this.mergedNamespaces.map(variable => variable.getName()));
}
if (this.syntheticNamedExports) {
assignmentArgs.push(this.module.getDefaultExport().getName());
}
if (members.length > 0) {
assignmentArgs.push(output);
}
output = `/*#__PURE__*/Object.assign(${assignmentArgs.join(`,${_}`)})`;
}
if (options.freeze) {
Expand Down
17 changes: 15 additions & 2 deletions src/ast/variables/SyntheticNamedExportVariable.ts
Expand Up @@ -3,7 +3,7 @@ import { InclusionContext } from '../ExecutionContext';
import ExportDefaultVariable from './ExportDefaultVariable';
import Variable from './Variable';

export default class SyntheticNamedExportVariableVariable extends Variable {
export default class SyntheticNamedExportVariable extends Variable {
context: AstContext;
defaultVariable: ExportDefaultVariable;
module: Module;
Expand All @@ -13,7 +13,16 @@ export default class SyntheticNamedExportVariableVariable extends Variable {
this.context = context;
this.module = context.module;
this.defaultVariable = defaultVariable;
this.setRenderNames(defaultVariable.getName(), name);
}

getName(): string {
const name = this.name;
const renderBaseName = this.defaultVariable.getName();
return `${renderBaseName}${getPropertyAccess(name)}`;
}

getOriginalVariable(): Variable {
return this.defaultVariable.getOriginalVariable();
}

include(context: InclusionContext) {
Expand All @@ -23,3 +32,7 @@ export default class SyntheticNamedExportVariableVariable extends Variable {
}
}
}

const getPropertyAccess = (name: string) => {
return /^(?!\d)[\w$]+$/.test(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
};
6 changes: 1 addition & 5 deletions src/ast/variables/Variable.ts
Expand Up @@ -49,7 +49,7 @@ export default class Variable implements ExpressionEntity {

getName(): string {
const name = this.renderName || this.name;
return this.renderBaseName ? `${this.renderBaseName}${getPropertyAccess(name)}` : name;
return this.renderBaseName ? `${this.renderBaseName}.${name}` : name;
}

getReturnExpressionWhenCalledAtPath(
Expand Down Expand Up @@ -107,7 +107,3 @@ export default class Variable implements ExpressionEntity {
return this.name;
}
}

const getPropertyAccess = (name: string) => {
return /^(?!\d)[\w$]+$/.test(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
};
9 changes: 6 additions & 3 deletions src/finalisers/es.ts
Expand Up @@ -5,7 +5,7 @@ import { FinaliserOptions } from './index';

export default function es(
magicString: MagicStringBundle,
{ intro, outro, dependencies, exports }: FinaliserOptions,
{ intro, outro, dependencies, exports, varOrConst }: FinaliserOptions,
options: OutputOptions
) {
const _ = options.compact ? '' : ' ';
Expand All @@ -15,7 +15,7 @@ export default function es(
if (importBlock.length > 0) intro += importBlock.join(n) + n + n;
if (intro) magicString.prepend(intro);

const exportBlock = getExportBlock(exports, _);
const exportBlock = getExportBlock(exports, _, varOrConst);
if (exportBlock.length) magicString.append(n + n + exportBlock.join(n).trim());
if (outro) magicString.append(outro);

Expand Down Expand Up @@ -110,13 +110,16 @@ function getImportBlock(dependencies: ChunkDependencies, _: string): string[] {
return importBlock;
}

function getExportBlock(exports: ChunkExports, _: string): string[] {
function getExportBlock(exports: ChunkExports, _: string, varOrConst: string): string[] {
const exportBlock: string[] = [];
const exportDeclaration: string[] = [];
for (const specifier of exports) {
if (specifier.exported === 'default') {
exportBlock.push(`export default ${specifier.local};`);
} else {
if (specifier.expression) {
exportBlock.push(`${varOrConst} ${specifier.local}${_}=${_}${specifier.expression};`);
}
exportDeclaration.push(
specifier.exported === specifier.local
? specifier.local
Expand Down
16 changes: 12 additions & 4 deletions src/finalisers/system.ts
Expand Up @@ -78,6 +78,16 @@ const getMissingExportsBlock = (exports: ChunkExports, _: string, t: string, n:
n
);

const getSyntheticExportsBlock = (exports: ChunkExports, _: string, t: string, n: string): string =>
getExportsBlock(
exports
.filter(expt => expt.expression)
.map(expt => ({ name: expt.exported, value: expt.local })),
_,
t,
n
);

export default function system(
magicString: MagicStringBundle,
{
Expand Down Expand Up @@ -194,13 +204,11 @@ export default function system(

const wrapperEnd =
`${n}${n}` +
getSyntheticExportsBlock(exports, _, t, n) +
getMissingExportsBlock(exports, _, t, n) +
`${t}${t}}${n}${t}}${options.compact ? '' : ';'}${n}});`;

if (intro) magicString.prepend(intro);
if (outro) magicString.append(outro);
return magicString
.indent(`${t}${t}${t}`)
.append(wrapperEnd)
.prepend(wrapperStart);
return magicString.indent(`${t}${t}${t}`).append(wrapperEnd).prepend(wrapperStart);
}

0 comments on commit aecd3e1

Please sign in to comment.