Skip to content

Commit

Permalink
Feat: add syntheticNamedExports (#3295)
Browse files Browse the repository at this point in the history
* Feat: add syntheticNamedExports

* fix namespaces

* remove getDefaultExport

* enable tests

* live references

* add tests

* fix test

* fix dead reference

* apply fixes

* add test for syntheticNamedExports in  transform

* apply feedback

* enable tests

* fix test

* unneeded exports check

* rename SyntheticNamedExport

* add docs

* Slightly clarify docs.

Co-authored-by: Lukas Taegert-Atkinson <lukastaegert@users.noreply.github.com>
  • Loading branch information
manucorporat and lukastaegert committed Jan 4, 2020
1 parent 7158c06 commit 345dca3
Show file tree
Hide file tree
Showing 43 changed files with 571 additions and 52 deletions.
62 changes: 58 additions & 4 deletions docs/05-plugin-development.md
Expand Up @@ -169,15 +169,33 @@ 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.

You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--moduleinfo) to find out the previous value of `moduleSideEffects` inside this hook.
If `true` is returned for `syntheticNamedExports`, this module will fallback the resolution of any missing named export to properties of the `default` export. The `transform` hook can override this. 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 values of `moduleSideEffects` and `syntheticNamedExports` inside this hook.

#### `options`
Type: `(options: InputOptions) => InputOptions | null`<br>
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. The `load` and `transform` hooks can override this. 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;
}
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

0 comments on commit 345dca3

Please sign in to comment.