Skip to content

Commit

Permalink
Provide normalized options (#3597)
Browse files Browse the repository at this point in the history
* Add test for #3467

* Extract multi-chunk option validation from output option parsing

* Reorder rollup entry by usage

* Get rid of chunk generation parameters

* Introduce Bundle class

* Make generate a little nicer

* Split option parsing

* Split merging from parsing

* Start implementing normalized input options

* Normalize treeshaking options and warnings

* Normalize input, manualChunks, signatures and inline validation

* Normalize remaining options

* Fix CLI object option handling

* Start normalizing output options

* Normalize file name options

* Normalize addons

* Normalize flags

* Normalize dynamicImportFunction and exports

* Normalize globals, name and paths

* Make options private in Graph

* Refactor option usage

* Fix types

* Remove unrelated TODOs

* Update docs

* Improve coverage

* Fix url in builtin warning
  • Loading branch information
lukastaegert committed May 27, 2020
1 parent 00e197f commit 9bff788
Show file tree
Hide file tree
Showing 109 changed files with 2,112 additions and 1,594 deletions.
2 changes: 1 addition & 1 deletion cli/cli.ts
@@ -1,7 +1,7 @@
import help from 'help.md';
import { version } from 'package.json';
import argParser from 'yargs-parser';
import { commandAliases } from '../src/utils/mergeOptions';
import { commandAliases } from '../src/utils/options/mergeOptions';
import run from './run/index';

const command = argParser(process.argv.slice(2), {
Expand Down
13 changes: 9 additions & 4 deletions cli/help.md
Expand Up @@ -56,10 +56,15 @@ Basic options:
--strictDeprecations Throw errors for deprecated features
--no-treeshake Disable tree-shaking optimisations
--no-treeshake.annotations Ignore pure call annotations
--no-treeshake.no-moduleSideEffects Assume modules have no side-effects
--no-treeshake.no-propertyReadSideEffects Ignore property access side-effects
--no-treeshake.no-tryCatchDeoptimization Do not turn off try-catch-tree-shaking
--no-treeshake.no-unknownGlobalSideEffects Assume unknown globals do not throw
--no-treeshake.moduleSideEffects Assume modules have no side-effects
--no-treeshake.propertyReadSideEffects Ignore property access side-effects
--no-treeshake.tryCatchDeoptimization Do not turn off try-catch-tree-shaking
--no-treeshake.unknownGlobalSideEffects Assume unknown globals do not throw
--watch.buildDelay <number> Throttle watch rebuilds
--no-watch.clearScreen Do not clear the screen when rebuilding
--watch.skipWrite Do not write files to disk when watching
--watch.exclude <files> Exclude files from being watched
--watch.include <files> Limit watching to specified files

Examples:

Expand Down
2 changes: 1 addition & 1 deletion cli/run/batchWarnings.ts
Expand Up @@ -80,7 +80,7 @@ const immediateHandlers: {
.map((name: string) => `'${name}'`)
.join(', ')} and '${warning.modules!.slice(-1)}'`;
stderr(
`Creating a browser bundle that depends on ${detail}. You might need to include https://www.npmjs.com/package/rollup-plugin-node-builtins`
`Creating a browser bundle that depends on ${detail}. You might need to include https://github.com/ionic-team/rollup-plugin-node-polyfills`
);
}
};
Expand Down
4 changes: 2 additions & 2 deletions cli/run/loadConfigFile.ts
Expand Up @@ -4,8 +4,8 @@ import { pathToFileURL } from 'url';
import * as rollup from '../../src/node-entry';
import { MergedRollupOptions } from '../../src/rollup/types';
import { error } from '../../src/utils/error';
import { mergeOptions } from '../../src/utils/mergeOptions';
import { GenericConfigObject } from '../../src/utils/parseOptions';
import { mergeOptions } from '../../src/utils/options/mergeOptions';
import { GenericConfigObject } from '../../src/utils/options/options';
import relativeId from '../../src/utils/relativeId';
import { stderr } from '../logging';
import batchWarnings, { BatchWarnings } from './batchWarnings';
Expand Down
2 changes: 1 addition & 1 deletion cli/run/loadConfigFromCommand.ts
@@ -1,5 +1,5 @@
import { MergedRollupOptions } from '../../src/rollup/types';
import { mergeOptions } from '../../src/utils/mergeOptions';
import { mergeOptions } from '../../src/utils/options/mergeOptions';
import batchWarnings, { BatchWarnings } from './batchWarnings';
import { addCommandPluginsToInputOptions } from './commandPlugins';
import { stdinName } from './stdin';
Expand Down
13 changes: 9 additions & 4 deletions docs/01-command-line-reference.md
Expand Up @@ -312,10 +312,15 @@ Many options have command line equivalents. In those cases, any arguments passed
--strictDeprecations Throw errors for deprecated features
--no-treeshake Disable tree-shaking optimisations
--no-treeshake.annotations Ignore pure call annotations
--no-treeshake.no-moduleSideEffects Assume modules have no side-effects
--no-treeshake.no-propertyReadSideEffects Ignore property access side-effects
--no-treeshake.no-tryCatchDeoptimization Do not turn off try-catch-tree-shaking
--no-treeshake.no-unknownGlobalSideEffects Assume unknown globals do not throw
--no-treeshake.moduleSideEffects Assume modules have no side-effects
--no-treeshake.propertyReadSideEffects Ignore property access side-effects
--no-treeshake.tryCatchDeoptimization Do not turn off try-catch-tree-shaking
--no-treeshake.unknownGlobalSideEffects Assume unknown globals do not throw
--watch.buildDelay <number> Throttle watch rebuilds
--no-watch.clearScreen Do not clear the screen when rebuilding
--watch.skipWrite Do not write files to disk when watching
--watch.exclude <files> Exclude files from being watched
--watch.include <files> Limit watching to specified files
```

The flags listed below are only available via the command line interface. All other flags correspond to and override their config file equivalents, see the [big list of options](guide/en/#big-list-of-options) for details.
Expand Down
4 changes: 2 additions & 2 deletions docs/05-plugin-development.md
Expand Up @@ -90,7 +90,7 @@ Kind: `async, parallel`<br>
Previous Hook: [`options`](guide/en/#options)<br>
Next Hook: [`resolveId`](guide/en/#resolveid) to resolve each entry point in parallel.

Called on each `rollup.rollup` build. This is the recommended hook to use when you need access to the options passed to `rollup.rollup()` as it will take the transformations by all [`options`](guide/en/#options) hooks into account.
Called on each `rollup.rollup` build. This is the recommended hook to use when you need access to the options passed to `rollup.rollup()` as it takes the transformations by all [`options`](guide/en/#options) hooks into account and also contains the right default values for unset options.

#### `load`
Type: `(id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null, syntheticNamedExports?: boolean | null }`<br>
Expand Down Expand Up @@ -409,7 +409,7 @@ Kind: `async, parallel`<br>
Previous Hook: [`outputOptions`](guide/en/#outputoptions)<br>
Next Hook: [`banner`](guide/en/#banner), [`footer`](guide/en/#footer), [`intro`](guide/en/#intro) and [`outro`](guide/en/#outro) run in parallel.
Called initially each time `bundle.generate()` or `bundle.write()` is called. To get notified when generation has completed, use the `generateBundle` and `renderError` hooks. This is the recommended hook to use when you need access to the output options passed to `bundle.generate()` or `bundle.write()` as it will take the transformations by all [`outputOptions`](guide/en/#outputoptions) hooks into account. It also receives the input options passed to `rollup.rollup()` so that plugins that can be used as output plugins, i.e. plugins that only use `generate` phase hooks, can get access to them.
Called initially each time `bundle.generate()` or `bundle.write()` is called. To get notified when generation has completed, use the `generateBundle` and `renderError` hooks. This is the recommended hook to use when you need access to the output options passed to `bundle.generate()` or `bundle.write()` as it takes the transformations by all [`outputOptions`](guide/en/#outputoptions) hooks into account and also contains the right default values for unset options. It also receives the input options passed to `rollup.rollup()` so that plugins that can be used as output plugins, i.e. plugins that only use `generate` phase hooks, can get access to them.
#### `resolveFileUrl`
Type: `({chunkId: string, fileName: string, format: string, moduleId: string, referenceId: string, relativePath: string}) => string | null`<br>
Expand Down
13 changes: 9 additions & 4 deletions docs/999-big-list-of-options.md
Expand Up @@ -1108,7 +1108,7 @@ class Impure {

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

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:
Expand Down Expand Up @@ -1276,6 +1276,7 @@ These options only take effect when running Rollup with the `--watch` flag, or u

#### watch.buildDelay
Type: `number`<br>
CLI: `--watch.buildDelay <number>`<br>
Default: `0`

Configures how long Rollup will wait for further changes until it triggers a rebuild in milliseconds. By default, Rollup does not wait but there is a small debounce timeout configured in the chokidar instance. Setting this to a value greater than `0` will mean that Rollup will only triger a rebuild if there was no change for the configured number of milliseconds. If several configurations are watched, Rollup will use the largest configured build delay.
Expand All @@ -1287,18 +1288,21 @@ An optional object of watch options that will be passed to the bundled [chokidar

#### watch.clearScreen
Type: `boolean`<br>
CLI: `--watch.clearScreen`/`--no-watch.clearScreen`<br>
Default: `true`

Whether to clear the screen when a rebuild is triggered.

#### watch.skipWrite
Type: `boolean`<br>
CLI: `--watch.skipWrite`/`--no-watch.skipWrite`<br>
Default: `false`

Whether to skip the `bundle.write()` step when a rebuild is triggered.

#### watch.exclude
Type: `string`
Type: `string`<br>
CLI: `--watch.exclude <files>`

Prevent files from being watched:

Expand All @@ -1313,9 +1317,10 @@ export default {
```

#### watch.include
Type: `string`
Type: `string`<br>
CLI: `--watch.include <files>`

Limit the file-watching to certain files:
Limit the file-watching to certain files. Note that this only filters the module graph but does not allow to add additional watch files:

```js
// rollup.config.js
Expand Down
172 changes: 172 additions & 0 deletions src/Bundle.ts
@@ -0,0 +1,172 @@
import Chunk from './Chunk';
import {
NormalizedInputOptions,
NormalizedOutputOptions,
OutputBundle,
OutputBundleWithPlaceholders,
OutputChunk
} from './rollup/types';
import { Addons, createAddons } from './utils/addons';
import commondir from './utils/commondir';
import { error, warnDeprecation } from './utils/error';
import { FILE_PLACEHOLDER } from './utils/FileEmitter';
import { basename, isAbsolute } from './utils/path';
import { PluginDriver } from './utils/PluginDriver';
import { timeEnd, timeStart } from './utils/timers';

export default class Bundle {
constructor(
private readonly outputOptions: NormalizedOutputOptions,
private readonly unsetOptions: Set<string>,
private readonly inputOptions: NormalizedInputOptions,
private readonly pluginDriver: PluginDriver,
private readonly chunks: Chunk[]
) {}

async generate(isWrite: boolean): Promise<OutputBundle> {
timeStart('GENERATE', 1);
const inputBase = commondir(getAbsoluteEntryModulePaths(this.chunks));
const outputBundle: OutputBundleWithPlaceholders = Object.create(null);
this.pluginDriver.setOutputBundle(outputBundle, this.outputOptions.assetFileNames);
try {
await this.pluginDriver.hookParallel('renderStart', [this.outputOptions, this.inputOptions]);
if (this.chunks.length > 1) {
validateOptionsForMultiChunkOutput(this.outputOptions);
}

const addons = await createAddons(this.outputOptions, this.pluginDriver);
for (const chunk of this.chunks) {
chunk.generateExports(this.outputOptions);
}
for (const chunk of this.chunks) {
chunk.preRender(this.outputOptions, inputBase, this.pluginDriver);
}
this.assignChunkIds(inputBase, addons, outputBundle);
assignChunksToBundle(this.chunks, outputBundle);

await Promise.all(
this.chunks.map(chunk => {
const outputChunk = outputBundle[chunk.id!] as OutputChunk;
return chunk
.render(this.outputOptions, addons, outputChunk, this.pluginDriver)
.then(rendered => {
outputChunk.code = rendered.code;
outputChunk.map = rendered.map;
});
})
);
} catch (error) {
await this.pluginDriver.hookParallel('renderError', [error]);
throw error;
}
await this.pluginDriver.hookSeq('generateBundle', [
this.outputOptions,
outputBundle as OutputBundle,
isWrite
]);
for (const key of Object.keys(outputBundle)) {
const file = outputBundle[key] as any;
if (!file.type) {
warnDeprecation(
'A plugin is directly adding properties to the bundle object in the "generateBundle" hook. This is deprecated and will be removed in a future Rollup version, please use "this.emitFile" instead.',
true,
this.inputOptions
);
file.type = 'asset';
}
}
this.pluginDriver.finaliseAssets();

timeEnd('GENERATE', 1);
return outputBundle as OutputBundle;
}

private assignChunkIds(inputBase: string, addons: Addons, bundle: OutputBundleWithPlaceholders) {
const entryChunks: Chunk[] = [];
const otherChunks: Chunk[] = [];
for (const chunk of this.chunks) {
(chunk.facadeModule && chunk.facadeModule.isUserDefinedEntryPoint
? entryChunks
: otherChunks
).push(chunk);
}

// make sure entry chunk names take precedence with regard to deconflicting
const chunksForNaming: Chunk[] = entryChunks.concat(otherChunks);
for (const chunk of chunksForNaming) {
if (this.outputOptions.file) {
chunk.id = basename(this.outputOptions.file);
} else if (this.inputOptions.preserveModules) {
chunk.id = chunk.generateIdPreserveModules(
inputBase,
this.outputOptions,
bundle,
this.unsetOptions
);
} else {
chunk.id = chunk.generateId(addons, this.outputOptions, bundle, true, this.pluginDriver);
}
bundle[chunk.id] = FILE_PLACEHOLDER;
}
}
}

function getAbsoluteEntryModulePaths(chunks: Chunk[]): string[] {
const absoluteEntryModulePaths: string[] = [];
for (const chunk of chunks) {
for (const entryModule of chunk.entryModules) {
if (isAbsolute(entryModule.id)) {
absoluteEntryModulePaths.push(entryModule.id);
}
}
}
return absoluteEntryModulePaths;
}

function validateOptionsForMultiChunkOutput(outputOptions: NormalizedOutputOptions) {
if (outputOptions.format === 'umd' || outputOptions.format === 'iife')
return error({
code: 'INVALID_OPTION',
message: 'UMD and IIFE output formats are not supported for code-splitting builds.'
});
if (typeof outputOptions.file === 'string')
return error({
code: 'INVALID_OPTION',
message:
'When building multiple chunks, the "output.dir" option must be used, not "output.file". ' +
'To inline dynamic imports, set the "inlineDynamicImports" option.'
});
if (outputOptions.sourcemapFile)
return error({
code: 'INVALID_OPTION',
message: '"output.sourcemapFile" is only supported for single-file builds.'
});
}

function assignChunksToBundle(
chunks: Chunk[],
outputBundle: OutputBundleWithPlaceholders
): OutputBundle {
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const facadeModule = chunk.facadeModule;

outputBundle[chunk.id!] = {
code: undefined as any,
dynamicImports: chunk.getDynamicImportIds(),
exports: chunk.getExportNames(),
facadeModuleId: facadeModule && facadeModule.id,
fileName: chunk.id,
imports: chunk.getImportIds(),
isDynamicEntry: chunk.isDynamicEntry,
isEntry: facadeModule !== null && facadeModule.isEntryPoint,
map: undefined,
modules: chunk.renderedModules,
get name() {
return chunk.getChunkName();
},
type: 'chunk'
} as OutputChunk;
}
return outputBundle as OutputBundle;
}

0 comments on commit 9bff788

Please sign in to comment.