Skip to content

Commit

Permalink
feat: support external star exports on namespace objects
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford committed Mar 31, 2020
1 parent 925e42c commit 1305abb
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 45 deletions.
64 changes: 41 additions & 23 deletions src/Module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import extractAssignedNames from 'rollup-pluginutils/src/extractAssignedNames';
import {
createHasEffectsContext,
createInclusionContext,
InclusionContext
InclusionContext,
} from './ast/ExecutionContext';
import ExportAllDeclaration from './ast/nodes/ExportAllDeclaration';
import ExportDefaultDeclaration from './ast/nodes/ExportDefaultDeclaration';
Expand Down Expand Up @@ -42,7 +42,7 @@ import {
ResolvedIdMap,
RollupError,
RollupWarning,
TransformModuleJSON
TransformModuleJSON,
} from './rollup/types';
import { error, Errors } from './utils/error';
import getCodeFrame from './utils/getCodeFrame';
Expand Down Expand Up @@ -101,6 +101,7 @@ export interface AstContext {
getReexports: () => string[];
importDescriptions: { [name: string]: ImportDescription };
includeDynamicImport: (node: ImportExpression) => void;
includeExternalReexportNamespace: (name: string) => ExternalVariable;
includeVariable: (context: InclusionContext, variable: Variable) => void;
isCrossChunkImport: (importDescription: ImportDescription) => boolean;
magicString: MagicString;
Expand All @@ -122,7 +123,7 @@ export interface AstContext {
export const defaultAcornOptions: acorn.Options = {
ecmaVersion: 2020,
preserveParens: false,
sourceType: 'module'
sourceType: 'module',
};

function tryParse(module: Module, Parser: typeof acorn.Parser, acornOptions: acorn.Options) {
Expand All @@ -131,7 +132,7 @@ function tryParse(module: Module, Parser: typeof acorn.Parser, acornOptions: aco
...defaultAcornOptions,
...acornOptions,
onComment: (block: boolean, text: string, start: number, end: number) =>
module.comments.push({ block, text, start, end })
module.comments.push({ block, text, start, end }),
});
} catch (err) {
let message = err.message.replace(/ \(\d+:\d+\)$/, '');
Expand All @@ -144,7 +145,7 @@ function tryParse(module: Module, Parser: typeof acorn.Parser, acornOptions: aco
{
code: 'PARSE_ERROR',
message,
parserError: err
parserError: err,
},
err.pos
);
Expand All @@ -163,15 +164,15 @@ function handleMissingExport(
message: `'${exportName}' is not exported by ${relativeId(
importedModule
)}, imported by ${relativeId(importingModule.id)}`,
url: `https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module`
url: `https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module`,
},
importerStart!
);
}

const MISSING_EXPORT_SHIM_DESCRIPTION: ExportDescription = {
identifier: null,
localName: MISSING_EXPORT_SHIM_VARIABLE
localName: MISSING_EXPORT_SHIM_VARIABLE,
};

function getVariableForExportNameRecursive(
Expand Down Expand Up @@ -288,17 +289,17 @@ export default class Module {
loc: {
column: location.column,
file: this.id,
line: location.line
line: location.line,
},
message: `Error when using sourcemap for reporting an error: ${e.message}`,
pos
pos,
});
}

props.loc = {
column: location.column,
file: this.id,
line: location.line
line: location.line,
};
props.frame = getCodeFrame(this.originalCode, location.line, location.column);
}
Expand Down Expand Up @@ -340,7 +341,7 @@ export default class Module {
return error({
code: Errors.SYNTHETIC_NAMED_EXPORTS_NEED_DEFAULT,
id: this.id,
message: `Modules with 'syntheticNamedExports' need a default export.`
message: `Modules with 'syntheticNamedExports' need a default export.`,
});
}
return this.defaultExport;
Expand Down Expand Up @@ -629,7 +630,7 @@ export default class Module {
sourcemapChain,
syntheticNamedExports,
transformDependencies,
transformFiles
transformFiles,
}: TransformModuleJSON & {
transformFiles?: EmittedFile[] | undefined;
}) {
Expand Down Expand Up @@ -664,7 +665,7 @@ export default class Module {

this.magicString = new MagicString(code, {
filename: (this.excludeFromSourcemap ? null : fileName)!, // don't include plugin helpers in sourcemap
indentExclusionRanges: []
indentExclusionRanges: [],
});
this.removeExistingSourceMap();

Expand All @@ -686,8 +687,9 @@ export default class Module {
getReexports: this.getReexports.bind(this),
importDescriptions: this.importDescriptions,
includeDynamicImport: this.includeDynamicImport.bind(this),
includeExternalReexportNamespace: this.includeExternalReexportNamespace.bind(this),
includeVariable: this.includeVariable.bind(this),
isCrossChunkImport: importDescription =>
isCrossChunkImport: (importDescription) =>
(importDescription.module as Module).chunk !== this.chunk,
magicString: this.magicString,
module: this,
Expand All @@ -705,7 +707,7 @@ export default class Module {
this.graph.treeshakingOptions.unknownGlobalSideEffects)!,
usesTopLevelAwait: false,
warn: this.warn.bind(this),
warnDeprecation: this.graph.warnDeprecation.bind(this.graph)
warnDeprecation: this.graph.warnDeprecation.bind(this.graph),
};

this.scope = new ModuleScope(this.graph.scope, this.astContext);
Expand All @@ -723,7 +725,7 @@ export default class Module {
ast: this.esTreeAst,
code: this.code,
customTransformCache: this.customTransformCache,
dependencies: Array.from(this.dependencies).map(module => module.id),
dependencies: Array.from(this.dependencies).map((module) => module.id),
id: this.id,
moduleSideEffects: this.moduleSideEffects,
originalCode: this.originalCode,
Expand All @@ -732,7 +734,7 @@ export default class Module {
sourcemapChain: this.sourcemapChain,
syntheticNamedExports: this.syntheticNamedExports,
transformDependencies: this.transformDependencies,
transformFiles: this.transformFiles
transformFiles: this.transformFiles,
};
}

Expand Down Expand Up @@ -793,7 +795,7 @@ export default class Module {

this.exports.default = {
identifier: node.variable.getAssignedVariableName(),
localName: 'default'
localName: 'default',
};
} else if (node instanceof ExportAllDeclaration) {
// export * from './other'
Expand All @@ -813,7 +815,7 @@ export default class Module {
specifier.type === NodeType.ExportNamespaceSpecifier ? '*' : specifier.local.name,
module: null as any, // filled in later,
source,
start: specifier.start
start: specifier.start,
};
}
} else if (node.declaration) {
Expand Down Expand Up @@ -854,7 +856,7 @@ export default class Module {
return this.error(
{
code: 'DUPLICATE_IMPORT',
message: `Duplicated import '${localName}'`
message: `Duplicated import '${localName}'`,
},
specifier.start
);
Expand All @@ -872,7 +874,7 @@ export default class Module {
module: null as any, // filled in later
name,
source,
start: specifier.start
start: specifier.start,
};
}
}
Expand All @@ -892,7 +894,9 @@ export default class Module {
}

private includeDynamicImport(node: ImportExpression) {
const resolution = (this.dynamicImports.find(dynamicImport => dynamicImport.node === node) as {
const resolution = (this.dynamicImports.find(
(dynamicImport) => dynamicImport.node === node
) as {
resolution: string | Module | ExternalModule | undefined;
}).resolution;
if (resolution instanceof Module) {
Expand All @@ -901,6 +905,20 @@ export default class Module {
}
}

private includeExternalReexportNamespace(name: string): ExternalVariable {
for (const module of this.exportAllModules) {
if (module instanceof ExternalModule) {
if (module.id === name) {
const externalVariable = module.getVariableForExportName('*');
externalVariable.include();
this.imports.add(externalVariable);
return externalVariable;
}
}
}
throw new Error(`Unable to find star reexport ExternalModule instance for ${name}`);
}

private includeVariable(context: InclusionContext, variable: Variable) {
const variableModule = variable.module;
if (!variable.included) {
Expand All @@ -926,7 +944,7 @@ export default class Module {
code: 'SHIMMED_EXPORT',
exporter: relativeId(this.id),
exportName: name,
message: `Missing export "${name}" has been shimmed in module ${relativeId(this.id)}.`
message: `Missing export "${name}" has been shimmed in module ${relativeId(this.id)}.`,
});
this.exports[name] = MISSING_EXPORT_SHIM_DESCRIPTION;
}
Expand Down
28 changes: 17 additions & 11 deletions src/ast/variables/NamespaceVariable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ 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 {
context: AstContext;
isNamespace!: true;
memberVariables: { [name: string]: Variable } = Object.create(null);
module: Module;
reexportedNamespaces: string[] = [];
private reexportedNamespaceVariables: ExternalVariable[] = [];

private containsExternalNamespace = false;
private referencedEarly = false;
private references: Identifier[] = [];
private syntheticNamedExports: boolean;
Expand Down Expand Up @@ -41,20 +43,16 @@ export default class NamespaceVariable extends Variable {

include(context: InclusionContext) {
if (!this.included) {
if (this.containsExternalNamespace) {
return this.context.error({
code: 'NAMESPACE_CANNOT_CONTAIN_EXTERNAL',
id: this.module.id,
message: `Cannot create an explicit namespace object for module "${this.context.getModuleName()}" because it contains a reexported external namespace`
});
}
this.included = true;
for (const identifier of this.references) {
if (identifier.context.getModuleExecIndex() <= this.context.getModuleExecIndex()) {
this.referencedEarly = true;
break;
}
}
for (const name of this.reexportedNamespaces) {
this.reexportedNamespaceVariables.push(this.context.includeExternalReexportNamespace(name));
}
if (this.context.preserveModules) {
for (const memberName of Object.keys(this.memberVariables))
this.memberVariables[memberName].include(context);
Expand All @@ -67,8 +65,11 @@ export default class NamespaceVariable extends Variable {

initialise() {
for (const name of this.context.getExports().concat(this.context.getReexports())) {
if (name[0] === '*' && name.length > 1) this.containsExternalNamespace = true;
this.memberVariables[name] = this.context.traceExport(name);
if (name[0] === '*' && name.length > 1) {
this.reexportedNamespaces.push(name.slice(1));
} else {
this.memberVariables[name] = this.context.traceExport(name);
}
}
}

Expand All @@ -77,7 +78,7 @@ export default class NamespaceVariable extends Variable {
const n = options.compact ? '' : '\n';
const t = options.indent;

const members = Object.keys(this.memberVariables).map(name => {
const members = Object.keys(this.memberVariables).map((name) => {
const original = this.memberVariables[name];

if (this.referencedEarly || original.isReassigned) {
Expand All @@ -99,6 +100,11 @@ export default class NamespaceVariable extends Variable {

const name = this.getName();
let output = `{${n}${members.join(`,${n}`)}${n}}`;
if (this.reexportedNamespaceVariables.length) {
output = `/*#__PURE__*/Object.assign(${this.reexportedNamespaceVariables
.map((v) => v.getName())
.join(', ')}, ${output})`;
}
if (this.syntheticNamedExports) {
output = `/*#__PURE__*/Object.assign(${output}, ${this.module.getDefaultExport().getName()})`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
const path = require('path');
const assert = require('assert');
const fs = require('fs');

module.exports = {
description:
'fails with a helpful error if creating a namespace object containing a reexported external namespace',
options: {
external: ['external']
external: ['fs'],
},
exports(ns) {
assert.strictEqual(ns.namespace.readFile, fs.readFile);
},
error: {
code: 'NAMESPACE_CANNOT_CONTAIN_EXTERNAL',
message:
'Cannot create an explicit namespace object for module "reexport" because it contains a reexported external namespace',
id: path.join(__dirname, 'reexport.js'),
watchFiles: [path.join(__dirname, 'main.js'), path.join(__dirname, 'reexport.js')]
}
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import * as namespace from './reexport.js';

console.log(namespace);
export { namespace };
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from 'external';
export * from 'fs';

0 comments on commit 1305abb

Please sign in to comment.