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

Add options and hooks to control module side effects #2844

Merged
merged 19 commits into from May 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5963243
Implement basic support for returning the `pure` flag from `resolveId`
lukastaegert May 3, 2019
e76e510
Handle conflicts between different resolutions by defaulting to impure
lukastaegert May 3, 2019
322435c
Add no-inferrable-types rule
lukastaegert May 4, 2019
787a98b
Handle conflicts between resolutions without an opinion about purenes…
lukastaegert May 5, 2019
0286cfb
Add purity to getModuleInfo, change logic to only use the first resol…
lukastaegert May 5, 2019
36ee1b6
Implement pureInternalModules and use as default
lukastaegert May 5, 2019
c278edf
Split test, regenerate iterator on demand
lukastaegert May 5, 2019
3ef7d36
Switch form pure to moduleSideEffects
lukastaegert May 6, 2019
2f41efc
Replace pureInternalModules with hasModuleSideEffects and refine logic
lukastaegert May 7, 2019
f4105d4
Implement load and transform hook handling
lukastaegert May 7, 2019
3b14521
Test all versions of the moduleSideEffects option
lukastaegert May 8, 2019
aa7498d
Explain deprecation alternatives in JSDoc
lukastaegert May 8, 2019
f6ebed8
Document new options
lukastaegert May 9, 2019
bddaa93
Try to fix Windows tests
lukastaegert May 9, 2019
f266045
Rename test folder, mark modules as executed in LocalVariable
lukastaegert May 11, 2019
fbf9bce
Refine and simplify interaction of plugins with the user option. Now …
lukastaegert May 12, 2019
e0ed3b9
Add an option for this.resolve to skip the plugin calling it
lukastaegert May 12, 2019
d73d100
Add "isEntry" to "getModuleInfo"
lukastaegert May 13, 2019
1ef16d8
Provide "isEntry" information already in the load hook.
lukastaegert May 13, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 27 additions & 16 deletions docs/05-plugins.md
Expand Up @@ -140,10 +140,14 @@ Kind: `async, parallel`
Cf. [`output.intro/output.outro`](guide/en#output-intro-output-outro).

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

Defines a custom loader. Returning `null` defers to other `load` functions (and eventually the default behavior of loading from the file system).
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 }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node.

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#this-getmoduleinfo-moduleid-string-moduleinfo) to find out the previous value of `moduleSideEffects` inside this hook.

#### `options`
Type: `(options: InputOptions) => InputOptions | null`<br>
Expand Down Expand Up @@ -224,10 +228,10 @@ resolveFileUrl({fileName}) {
```

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

Defines a custom resolver. A resolver can be useful for e.g. locating third-party dependencies. Returning `null` defers to other `resolveId` functions (and eventually the default resolution behavior); returning `false` signals that `source` should be treated as an external module and not included in the bundle.
Defines a custom resolver. A resolver can be useful for e.g. locating third-party dependencies. Returning `null` defers to other `resolveId` functions and eventually the default resolution behavior; returning `false` signals that `source` should be treated as an external module and not included in the bundle.

If you return an object, then it is possible to resolve an import to a different id while excluding it from the bundle at the same time. This allows you to replace dependencies with external dependencies without the need for the user to mark them as "external" manually via the `external` option:

Expand All @@ -240,6 +244,8 @@ resolveId(source) {
}
```

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.

#### `resolveImportMeta`
Type: `(property: string | null, {chunkId: string, moduleId: string, format: string}) => string | null`<br>
Kind: `sync, first`
Expand All @@ -263,11 +269,16 @@ 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 | { code: string, map?: string | SourceMap, ast? : ESTree.Program } | null`
<br>
Type: `(code: string, id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null }`<br>
Kind: `async, sequential`

Can be used to transform individual modules. Note that in watch mode, the result of this hook is cached when rebuilding and the hook is only triggered again for a module `id` if either the `code` of the module has changed or a file has changed that was added via `this.addWatchFile` the last time the hook was triggered for this module.
Can be used to transform individual modules. 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 }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node.

Note that in watch mode, the result of this hook is cached when rebuilding and the hook is only triggered again for a module `id` if either the `code` of the module has changed or a file has changed that was added via `this.addWatchFile` the last time the hook was triggered for this module.

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`.

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

#### `watchChange`
Type: `(id: string) => void`<br>
Expand Down Expand Up @@ -346,11 +357,13 @@ Get the file name of an emitted chunk. The file name will be relative to `output

Returns additional information about the module in question in the form

```js
```
{
id, // the id of the module, for convenience
isExternal, // for external modules that are not included in the graph
importedIds // the module ids imported by this module
id: string, // the id of the module, for convenience
isEntry: boolean, // is this a user- or plugin-defined entry point
isExternal: boolean, // for external modules that are not included in the graph
importedIds: string[], // the module ids imported by this module
hasModuleSideEffects: boolean // are imports of this module included if nothing is imported from it
}
```

Expand All @@ -374,9 +387,11 @@ or converted into an Array via `Array.from(this.moduleIds)`.

Use Rollup's internal acorn instance to parse code to an AST.

#### `this.resolve(source: string, importer: string) => Promise<{id: string, external: boolean} | null>`
#### `this.resolve(source: string, importer: string, options?: {skipSelf: boolean}) => Promise<{id: string, external: boolean} | null>`
Resolve imports to module ids (i.e. file names) using the same plugins that Rollup uses, and determine if an import should be external. If `null` is returned, the import could not be resolved by Rollup or any plugin but was not explicitly marked as external by the user.

If you pass `skipSelf: true`, then the `resolveId` hook of the plugin from which `this.resolve` is called will be skipped when resolving.

#### `this.setAssetSource(assetReferenceId: string, source: string | Buffer) => void`

Set the deferred source of an asset.
Expand Down Expand Up @@ -502,10 +517,6 @@ export const size = 6;

If you build this code, both the main chunk and the worklet will share the code from `config.js` via a shared chunk. This enables us to make use of the browser cache to reduce transmitted data and speed up loading the worklet.

### Advanced Loaders

The `load` hook can optionally return a `{ code, ast }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node.

### Transformers

Transformer plugins (i.e. those that return a `transform` function for e.g. transpiling non-JS files) should support `options.include` and `options.exclude`, both of which can be a minimatch pattern or an array of minimatch patterns. If `options.include` is omitted or of zero length, files should be included by default; otherwise they should only be included if the ID matches one of the patterns.
Expand Down
111 changes: 84 additions & 27 deletions docs/999-big-list-of-options.md
Expand Up @@ -729,32 +729,13 @@ Default: `false`
If this option is provided, bundling will not fail if bindings are imported from a file that does not define these bindings. Instead, new variables will be created for these bindings with the value `undefined`.

#### treeshake
Type: `boolean | { propertyReadSideEffects?: boolean, annotations?: boolean, pureExternalModules?: boolean }`<br>
Type: `boolean | { annotations?: boolean, moduleSideEffects?: ModuleSideEffectsOption, propertyReadSideEffects?: boolean }`<br>
CLI: `--treeshake`/`--no-treeshake`<br>
Default: `true`

Whether or not to apply tree-shaking and to fine-tune the tree-shaking process. Setting this option to `false` will produce bigger bundles but may improve build performance. If you discover a bug caused by the tree-shaking algorithm, please file an issue!
Setting this option to an object implies tree-shaking is enabled and grants the following additional options:

**treeshake.propertyReadSideEffects**
Type: `boolean`<br>
CLI: `--treeshake.propertyReadSideEffects`/`--no-treeshake.propertyReadSideEffects`<br>
Default: `true`

If `false`, assume reading a property of an object never has side-effects. Depending on your code, disabling this option can significantly reduce bundle size but can potentially break functionality if you rely on getters or errors from illegal property access.

```javascript
// Will be removed if treeshake.propertyReadSideEffects === false
const foo = {
get bar() {
console.log('effect');
return 'bar';
}
}
const result = foo.bar;
const illegalAccess = foo.quux.tooDeep;
```

**treeshake.annotations**<br>
Type: `boolean`<br>
CLI: `--treeshake.annotations`/`--no-treeshake.annotations`<br>
Expand All @@ -774,12 +755,12 @@ class Impure {
/*@__PURE__*/new Impure();
```

**treeshake.pureExternalModules**<br>
Type: `boolean`<br>
CLI: `--treeshake.pureExternalModules`/`--no-treeshake.pureExternalModules`<br>
Default: `false`
**treeshake.moduleSideEffects**<br>
Type: `boolean | "no-external" | string[] | (id: string, external: boolean) => boolean`<br>
CLI: `--treeshake.moduleSideEffects`/`--no-treeshake.moduleSideEffects`<br>
Default: `true`

If `true`, assume external dependencies from which nothing is imported do not have other side-effects like mutating global variables or logging.
If `false`, assume modules and external dependencies from which nothing is imported do not have other side-effects like mutating global variables or logging without checking. For external dependencies, this will suppress empty imports:

```javascript
// input file
Expand All @@ -789,17 +770,61 @@ console.log(42);
```

```javascript
// output with treeshake.pureExternalModules === false
// output with treeshake.moduleSideEffects === true
import 'external-a';
import 'external-b';
console.log(42);
```

```javascript
// output with treeshake.pureExternalModules === true
// output with treeshake.moduleSideEffects === false
console.log(42);
```

For non-external modules, `false` will not include any statements from a module unless at least one import from this module is included:

```javascript
// input file a.js
import {unused} from './b.js';
console.log(42);

// input file b.js
console.log('side-effect');
```

```javascript
// output with treeshake.moduleSideEffects === true
console.log('side-effect');

console.log(42);
```

```javascript
// output with treeshake.moduleSideEffects === false
console.log(42);
```

You can also supply a list of modules with side-effects or a function to determine it for each module individually. The value `"no-external"` will only remove external imports if possible and is equivalent to the function `(id, external) => !external`;

**treeshake.propertyReadSideEffects**
Type: `boolean`<br>
CLI: `--treeshake.propertyReadSideEffects`/`--no-treeshake.propertyReadSideEffects`<br>
Default: `true`

If `false`, assume reading a property of an object never has side-effects. Depending on your code, disabling this option can significantly reduce bundle size but can potentially break functionality if you rely on getters or errors from illegal property access.

```javascript
// Will be removed if treeshake.propertyReadSideEffects === false
const foo = {
get bar() {
console.log('effect');
return 'bar';
}
}
const result = foo.bar;
const illegalAccess = foo.quux.tooDeep;
```

### Experimental options

These options reflect new features that have not yet been fully finalized. Availability, behaviour and usage may therefore be subject to change between minor versions.
Expand Down Expand Up @@ -903,3 +928,35 @@ export default {
}
};
```

### Deprecated options

☢️ These options have been deprecated and may be removed in a future Rollup version.

#### treeshake.pureExternalModules
Type: `boolean | string[] | (id: string) => boolean | null`<br>
CLI: `--treeshake.pureExternalModules`/`--no-treeshake.pureExternalModules`<br>
Default: `false`

If `true`, assume external dependencies from which nothing is imported do not have other side-effects like mutating global variables or logging.

```javascript
// input file
import {unused} from 'external-a';
import 'external-b';
console.log(42);
```

```javascript
// output with treeshake.pureExternalModules === false
import 'external-a';
import 'external-b';
console.log(42);
```

```javascript
// output with treeshake.pureExternalModules === true
console.log(42);
```

You can also supply a list of external ids to be considered pure or a function that is called whenever an external import could be removed.
8 changes: 4 additions & 4 deletions src/Chunk.ts
Expand Up @@ -114,10 +114,10 @@ export function isChunkRendered(chunk: Chunk): boolean {
export default class Chunk {
entryModules: Module[] = [];
execIndex: number;
exportMode: string = 'named';
exportMode = 'named';
facadeModule: Module | null = null;
graph: Graph;
hasDynamicImport: boolean = false;
hasDynamicImport = false;
id: string = undefined;
indentString: string = undefined;
isEmpty: boolean;
Expand All @@ -135,7 +135,7 @@ export default class Chunk {
private exportNames: { [name: string]: Variable } = Object.create(null);
private exports = new Set<Variable>();
private imports = new Set<Variable>();
private needsExportsShim: boolean = false;
private needsExportsShim = false;
private renderedDeclarations: {
dependencies: ChunkDependencies;
exports: ChunkExports;
Expand Down Expand Up @@ -753,7 +753,7 @@ export default class Chunk {
if (depModule instanceof Module) {
dependency = depModule.chunk;
} else {
if (!depModule.used && this.graph.isPureExternalModule(depModule.id)) {
if (!(depModule.used || depModule.moduleSideEffects)) {
continue;
}
dependency = depModule;
Expand Down
11 changes: 6 additions & 5 deletions src/ExternalModule.ts
Expand Up @@ -10,24 +10,25 @@ export default class ExternalModule {
execIndex: number;
exportedVariables: Map<ExternalVariable, string>;
exportsNames = false;
exportsNamespace: boolean = false;
exportsNamespace = false;
id: string;
isEntryPoint = false;
isExternal = true;
mostCommonSuggestion: number = 0;
moduleSideEffects: boolean;
mostCommonSuggestion = 0;
nameSuggestions: { [name: string]: number };
reexported: boolean = false;
reexported = false;
renderPath: string = undefined;
renormalizeRenderPath = false;
used = false;
variableName: string;

private graph: Graph;

constructor({ graph, id }: { graph: Graph; id: string }) {
constructor(graph: Graph, id: string, moduleSideEffects: boolean) {
this.graph = graph;
this.id = id;
this.execIndex = Infinity;
this.moduleSideEffects = moduleSideEffects;

const parts = id.split(/[\\/]/);
this.variableName = makeLegal(parts.pop());
Expand Down
27 changes: 11 additions & 16 deletions src/Graph.ts
Expand Up @@ -67,10 +67,9 @@ export default class Graph {
curChunkIndex = 0;
deoptimizationTracker: EntityPathTracker;
getModuleContext: (id: string) => string;
isPureExternalModule: (id: string) => boolean;
moduleById = new Map<string, Module | ExternalModule>();
moduleLoader: ModuleLoader;
needsTreeshakingPass: boolean = false;
needsTreeshakingPass = false;
phase: BuildPhase = BuildPhase.LOAD_AND_PARSE;
pluginDriver: PluginDriver;
preserveModules: boolean;
Expand Down Expand Up @@ -114,23 +113,17 @@ export default class Graph {
this.treeshakingOptions = options.treeshake
? {
annotations: (<TreeshakingOptions>options.treeshake).annotations !== false,
moduleSideEffects: (<TreeshakingOptions>options.treeshake).moduleSideEffects,
propertyReadSideEffects:
(<TreeshakingOptions>options.treeshake).propertyReadSideEffects !== false,
pureExternalModules: (<TreeshakingOptions>options.treeshake).pureExternalModules
}
: { propertyReadSideEffects: true, annotations: true, pureExternalModules: false };
if (this.treeshakingOptions.pureExternalModules === true) {
this.isPureExternalModule = () => true;
} else if (typeof this.treeshakingOptions.pureExternalModules === 'function') {
this.isPureExternalModule = this.treeshakingOptions.pureExternalModules;
} else if (Array.isArray(this.treeshakingOptions.pureExternalModules)) {
const pureExternalModules = new Set(this.treeshakingOptions.pureExternalModules);
this.isPureExternalModule = id => pureExternalModules.has(id);
} else {
this.isPureExternalModule = () => false;
}
} else {
this.isPureExternalModule = () => false;
: {
annotations: true,
moduleSideEffects: true,
propertyReadSideEffects: true,
pureExternalModules: false
};
}

this.contextParse = (code: string, options: acorn.Options = {}) =>
Expand Down Expand Up @@ -193,7 +186,9 @@ export default class Graph {
this.moduleById,
this.pluginDriver,
options.external,
typeof options.manualChunks === 'function' && options.manualChunks
typeof options.manualChunks === 'function' && options.manualChunks,
this.treeshake ? this.treeshakingOptions.moduleSideEffects : null,
this.treeshake ? this.treeshakingOptions.pureExternalModules : false
);
}

Expand Down