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

Feat: add syntheticNamedExports #3295

Merged
merged 18 commits into from Jan 4, 2020
Merged
60 changes: 57 additions & 3 deletions docs/05-plugin-development.md
Expand Up @@ -169,14 +169,32 @@ Phase: `generate`
Cf. [`output.intro/output.outro`](guide/en/#outputintrooutputoutro).

#### `load`
Type: `(id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null }`<br>
Type: `(id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null, syntheticNamedExports?: boolean | null }`<br>
Kind: `async, first`<br>
Phase: `build`

Defines a custom loader. Returning `null` defers to other `load` functions (and eventually the default behavior of loading from the file system). To prevent additional parsing overhead in case e.g. this hook already used `this.parse` to generate an AST for some reason, this hook can optionally return a `{ code, ast, map }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node. If the transformation does not move code, you can preserve existing sourcemaps by setting `map` to `null`. Otherwise you might need to generate the source map. See [the section on source code transformations](#source-code-transformations).

If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included in the bundle without checking for actual side-effects inside the module. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `true`. The `transform` hook can override this.

If `true` is returned for `syntheticNamedExports`, this module will fallback the resolution of any missing named export to properties of the `default` export. This option allows to have dynamic named exports that might not be declared in the module, such as in this example:

**dep.js: (`{syntheticNamedExports: true}`)**

```
export default {
foo: 42,
bar: 'hello'
}
```

**main.js: (entry point)**

```js
import { foo, bar } from './dep.js'
console.log(foo, bar);
```

You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--moduleinfo) to find out the previous value of `moduleSideEffects` inside this hook.

#### `options`
Expand Down Expand Up @@ -268,7 +286,7 @@ resolveFileUrl({fileName}) {
```

#### `resolveId`
Type: `(source: string, importer: string) => string | false | null | {id: string, external?: boolean, moduleSideEffects?: boolean | null}`<br>
Type: `(source: string, importer: string) => string | false | null | {id: string, external?: boolean, moduleSideEffects?: boolean | null, syntheticNamedExports?: boolean | null}`<br>
Kind: `async, first`<br>
Phase: `build`

Expand All @@ -289,6 +307,24 @@ Relative ids, i.e. starting with `./` or `../`, will **not** be renormalized whe

If `false` is returned for `moduleSideEffects` in the first hook that resolves a module id and no other module imports anything from this module, then this module will not be included without checking for actual side-effects inside the module. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the `treeshake.moduleSideEffects` option or default to `true`. The `load` and `transform` hooks can override this.

If `true` is returned for `syntheticNamedExports`, this module will fallback the resolution of any missing named export to properties of the `default` export. This option allows to have dynamic named exports that might not be declared in the module, such as in this example:

**dep.js: (`{syntheticNamedExports: true}`)**

```
export default {
foo: 42,
bar: 'hello'
}
```

**main.js: (entry point)**

```js
import { foo, bar } from './dep.js'
console.log(foo, bar);
```

#### `resolveImportMeta`
Type: `(property: string | null, {chunkId: string, moduleId: string, format: string}) => string | null`<br>
Kind: `sync, first`<br>
Expand All @@ -313,7 +349,7 @@ resolveImportMeta(property, {moduleId}) {
Note that since this hook has access to the filename of the current chunk, its return value will not be considered when generating the hash of this chunk.

#### `transform`
Type: `(code: string, id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null }`<br>
Type: `(code: string, id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null, syntheticNamedExports?: boolean | null }`<br>
Kind: `async, sequential`<br>
Phase: `build`

Expand All @@ -323,6 +359,24 @@ Note that in watch mode, the result of this hook is cached when rebuilding and t

If `false` is returned for `moduleSideEffects` and no other module imports anything from this module, then this module will not be included without checking for actual side-effects inside the module. If `true` is returned, Rollup will use its default algorithm to include all statements in the module that have side-effects (such as modifying a global or exported variable). If `null` is returned or the flag is omitted, then `moduleSideEffects` will be determined by the first `resolveId` hook that resolved this module, the `treeshake.moduleSideEffects` option, or eventually default to `true`.

If `true` is returned for `syntheticNamedExports`, this module will fallback the resolution of any missing named export to properties of the `default` export. This option allows to have dynamic named exports that might not be declared in the module, such as in this example:

**dep.js: (`{syntheticNamedExports: true}`)**

```
export default {
foo: 42,
bar: 'hello'
}
```

**main.js: (entry point)**

```js
import { foo, bar } from './dep.js'
console.log(foo, bar);
```

You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--moduleinfo) to find out the previous value of `moduleSideEffects` inside this hook.

#### `watchChange`
Expand Down
57 changes: 51 additions & 6 deletions src/Module.ts
Expand Up @@ -22,9 +22,11 @@ import TemplateLiteral from './ast/nodes/TemplateLiteral';
import VariableDeclaration from './ast/nodes/VariableDeclaration';
import ModuleScope from './ast/scopes/ModuleScope';
import { PathTracker, UNKNOWN_PATH } from './ast/utils/PathTracker';
import ExportDefaultVariable from './ast/variables/ExportDefaultVariable';
import ExportShimVariable from './ast/variables/ExportShimVariable';
import ExternalVariable from './ast/variables/ExternalVariable';
import NamespaceVariable from './ast/variables/NamespaceVariable';
import SyntheticNamedExportVariable from './ast/variables/SyntheticNamedExportVariable';
import Variable from './ast/variables/Variable';
import Chunk from './Chunk';
import ExternalModule from './ExternalModule';
Expand All @@ -39,7 +41,7 @@ import {
RollupWarning,
TransformModuleJSON
} from './rollup/types';
import { error } from './utils/error';
import { error, Errors } from './utils/error';
import getCodeFrame from './utils/getCodeFrame';
import { getOriginalLocation } from './utils/getOriginalLocation';
import { makeLegal } from './utils/identifierHelpers';
Expand Down Expand Up @@ -206,6 +208,7 @@ export default class Module {
scope!: ModuleScope;
sourcemapChain!: DecodedSourceMapOrMissing[];
sources = new Set<string>();
syntheticNamedExports: boolean;
transformFiles?: EmittedFile[];
userChunkNames = new Set<string>();
usesTopLevelAwait = false;
Expand All @@ -214,19 +217,28 @@ export default class Module {
private ast!: Program;
private astContext!: AstContext;
private context: string;
private defaultExport: ExportDefaultVariable | null | undefined = null;
private esTreeAst!: ESTree.Program;
private graph: Graph;
private magicString!: MagicString;
private namespaceVariable: NamespaceVariable | null = null;
private syntheticExports = new Map<string, SyntheticNamedExportVariable>();
private transformDependencies: string[] = [];
private transitiveReexports: string[] | null = null;

constructor(graph: Graph, id: string, moduleSideEffects: boolean, isEntry: boolean) {
constructor(
graph: Graph,
id: string,
moduleSideEffects: boolean,
syntheticNamedExports: boolean,
isEntry: boolean
) {
this.id = id;
this.graph = graph;
this.excludeFromSourcemap = /\0/.test(id);
this.context = graph.getModuleContext(id);
this.moduleSideEffects = moduleSideEffects;
this.syntheticNamedExports = syntheticNamedExports;
this.isEntryPoint = isEntry;
}

Expand Down Expand Up @@ -299,6 +311,21 @@ export default class Module {
return allExportNames;
}

getDefaultExport() {
if (this.defaultExport === null) {
this.defaultExport = undefined;
this.defaultExport = this.getVariableForExportName('default') as ExportDefaultVariable;
}
if (!this.defaultExport) {
return error({
code: Errors.SYNTHETIC_NAMED_EXPORTS_NEED_DEFAULT,
id: this.id,
message: `Modules with 'syntheticNamedExports' need a default export.`
});
}
return this.defaultExport;
}

getDynamicImportExpressions(): (string | Node)[] {
return this.dynamicImports.map(({ node }) => {
const importArgument = node.source;
Expand Down Expand Up @@ -342,7 +369,7 @@ export default class Module {

getOrCreateNamespace(): NamespaceVariable {
if (!this.namespaceVariable) {
this.namespaceVariable = new NamespaceVariable(this.astContext);
this.namespaceVariable = new NamespaceVariable(this.astContext, this.syntheticNamedExports);
this.namespaceVariable.initialise();
}
return this.namespaceVariable;
Expand Down Expand Up @@ -439,9 +466,22 @@ export default class Module {

// we don't want to create shims when we are just
// probing export * modules for exports
if (this.graph.shimMissingExports && !isExportAllSearch) {
this.shimMissingExport(name);
return this.exportShimVariable;
if (!isExportAllSearch) {
if (this.syntheticNamedExports) {
let syntheticExport = this.syntheticExports.get(name);
if (!syntheticExport) {
const defaultExport = this.getDefaultExport();
syntheticExport = new SyntheticNamedExportVariable(this.astContext, name, defaultExport);
this.syntheticExports.set(name, syntheticExport);
return syntheticExport;
}
return syntheticExport;
}

if (this.graph.shimMissingExports) {
this.shimMissingExport(name);
return this.exportShimVariable;
}
}
return undefined as any;
}
Expand Down Expand Up @@ -534,6 +574,7 @@ export default class Module {
originalSourcemap,
resolvedIds,
sourcemapChain,
syntheticNamedExports,
transformDependencies,
transformFiles
}: TransformModuleJSON & {
Expand All @@ -551,6 +592,9 @@ export default class Module {
if (typeof moduleSideEffects === 'boolean') {
this.moduleSideEffects = moduleSideEffects;
}
if (typeof syntheticNamedExports === 'boolean') {
this.syntheticNamedExports = syntheticNamedExports;
}

timeStart('generate ast', 3);

Expand Down Expand Up @@ -633,6 +677,7 @@ export default class Module {
originalSourcemap: this.originalSourcemap,
resolvedIds: this.resolvedIds,
sourcemapChain: this.sourcemapChain,
syntheticNamedExports: this.syntheticNamedExports,
transformDependencies: this.transformDependencies,
transformFiles: this.transformFiles
};
Expand Down
49 changes: 36 additions & 13 deletions src/ModuleLoader.ts
Expand Up @@ -17,6 +17,7 @@ import {
errBadLoader,
errCannotAssignModuleToChunk,
errEntryCannotBeExternal,
errExternalSyntheticExports,
errInternalIdCannotBeExternal,
errInvalidOption,
errNamespaceConflict,
Expand Down Expand Up @@ -243,7 +244,7 @@ export class ModuleLoader {
module.id,
(module.resolvedIds[source] =
module.resolvedIds[source] ||
this.handleMissingImports(await this.resolveId(source, module.id), source, module.id))
this.handleResolveId(await this.resolveId(source, module.id), source, module.id))
)
) as Promise<unknown>[]),
...module.getDynamicImportExpressions().map((specifier, index) =>
Expand Down Expand Up @@ -272,6 +273,7 @@ export class ModuleLoader {
id: string,
importer: string,
moduleSideEffects: boolean,
syntheticNamedExports: boolean,
isEntry: boolean
): Promise<Module> {
const existingModule = this.modulesById.get(id);
Expand All @@ -280,7 +282,13 @@ export class ModuleLoader {
return Promise.resolve(existingModule);
}

const module: Module = new Module(this.graph, id, moduleSideEffects, isEntry);
const module: Module = new Module(
this.graph,
id,
moduleSideEffects,
syntheticNamedExports,
isEntry
);
this.modulesById.set(id, module);
this.graph.watchFiles[id] = true;
const manualChunkAlias = this.getManualChunk(id);
Expand Down Expand Up @@ -321,6 +329,9 @@ export class ModuleLoader {
if (typeof sourceDescription.moduleSideEffects === 'boolean') {
module.moduleSideEffects = sourceDescription.moduleSideEffects;
}
if (typeof sourceDescription.syntheticNamedExports === 'boolean') {
module.syntheticNamedExports = sourceDescription.syntheticNamedExports;
manucorporat marked this conversation as resolved.
Show resolved Hide resolved
}
return transform(this.graph, sourceDescription, module);
})
.then((source: TransformModuleJSON | ModuleJSON) => {
Expand Down Expand Up @@ -370,11 +381,17 @@ export class ModuleLoader {
}
return Promise.resolve(externalModule);
} else {
return this.fetchModule(resolvedId.id, importer, resolvedId.moduleSideEffects, false);
return this.fetchModule(
resolvedId.id,
importer,
resolvedId.moduleSideEffects,
resolvedId.syntheticNamedExports,
false
);
}
}

private handleMissingImports(
private handleResolveId(
resolvedId: ResolvedId | null,
source: string,
importer: string
Expand All @@ -387,8 +404,13 @@ export class ModuleLoader {
return {
external: true,
id: source,
moduleSideEffects: this.hasModuleSideEffects(source, true)
moduleSideEffects: this.hasModuleSideEffects(source, true),
syntheticNamedExports: false
};
} else {
if (resolvedId.external && resolvedId.syntheticNamedExports) {
this.graph.warn(errExternalSyntheticExports(source, importer));
}
}
return resolvedId;
}
Expand All @@ -407,7 +429,7 @@ export class ModuleLoader {
: resolveIdResult;

if (typeof id === 'string') {
return this.fetchModule(id, undefined as any, true, isEntry);
return this.fetchModule(id, undefined as any, true, false, isEntry);
}
return error(errUnresolvedEntry(unresolvedId));
});
Expand All @@ -420,6 +442,7 @@ export class ModuleLoader {
let id = '';
let external = false;
let moduleSideEffects = null;
let syntheticNamedExports = false;
if (resolveIdResult) {
if (typeof resolveIdResult === 'object') {
id = resolveIdResult.id;
Expand All @@ -429,6 +452,9 @@ export class ModuleLoader {
if (typeof resolveIdResult.moduleSideEffects === 'boolean') {
moduleSideEffects = resolveIdResult.moduleSideEffects;
}
if (typeof resolveIdResult.syntheticNamedExports === 'boolean') {
syntheticNamedExports = resolveIdResult.syntheticNamedExports;
}
} else {
if (this.isExternal(resolveIdResult, importer, true)) {
external = true;
Expand All @@ -448,7 +474,8 @@ export class ModuleLoader {
moduleSideEffects:
typeof moduleSideEffects === 'boolean'
? moduleSideEffects
: this.hasModuleSideEffects(id, external)
: this.hasModuleSideEffects(id, external),
syntheticNamedExports
};
}

Expand Down Expand Up @@ -478,13 +505,9 @@ export class ModuleLoader {
if (resolution == null) {
return (module.resolvedIds[specifier] =
module.resolvedIds[specifier] ||
this.handleMissingImports(
await this.resolveId(specifier, module.id),
specifier,
module.id
));
this.handleResolveId(await this.resolveId(specifier, module.id), specifier, module.id));
}
return this.handleMissingImports(
return this.handleResolveId(
this.normalizeResolveIdResult(resolution, importer, specifier),
specifier,
importer
Expand Down