diff --git a/cli/run/build.ts b/cli/run/build.ts index a744bec485e..ec2e5abe104 100644 --- a/cli/run/build.ts +++ b/cli/run/build.ts @@ -66,10 +66,9 @@ export default function build( }); } - return outputOptions.reduce( - (prev, output) => prev.then(() => bundle.write(output) as Promise), - Promise.resolve() - ).then(() => bundle) + return Promise.all(outputOptions.map(output => bundle.write(output) as Promise)).then( + () => bundle + ); }) .then((bundle: RollupBuild | null) => { if (!silent) { diff --git a/docs/01-command-line-reference.md b/docs/01-command-line-reference.md index 8970e765fa4..e4e76c3687e 100755 --- a/docs/01-command-line-reference.md +++ b/docs/01-command-line-reference.md @@ -66,6 +66,7 @@ export default { // can be an array (for multiple inputs) format, // required globals, name, + plugins, // advanced output options assetFileNames, diff --git a/docs/02-javascript-api.md b/docs/02-javascript-api.md index a209163c0de..992802dbe16 100755 --- a/docs/02-javascript-api.md +++ b/docs/02-javascript-api.md @@ -116,6 +116,7 @@ const outputOptions = { format, // required globals, name, + plugins, // advanced output options assetFileNames, diff --git a/docs/04-tutorial.md b/docs/04-tutorial.md index 73b3e811300..6ed34605e60 100755 --- a/docs/04-tutorial.md +++ b/docs/04-tutorial.md @@ -227,17 +227,70 @@ Run Rollup with `npm run build`. The result should look like this: ```js 'use strict'; -const version = "1.0.0"; +var version = "1.0.0"; -const main = function () { +function main () { console.log('version ' + version); -}; +} module.exports = main; ``` _Note: Only the data we actually need gets imported – `name` and `devDependencies` and other parts of `package.json` are ignored. That's **tree-shaking** in action._ +### Using output plugins + +Some plugins can also be applied specifically to some outputs. See [plugin hooks](guide/en/#hooks) for the technical details of what output-specific plugins can do. In a nut-shell, those plugins can only modify code after the main analysis of Rollup has completed. Rollup will warn if an incompatible plugin is used as an output-specific plugin. One possible use-case is minification of bundles to be consumed in a browser. + +Let us extend the previous example to provide a minified build together with the non-minified one. To that end, we install `rollup-plugin-terser`: + +```console +npm install --save-dev rollup-plugin-terser +``` + +Edit your `rollup.config.js` file to add a second minified output. As format, we choose `iife`. This format wraps the code so that it can be consumed via a `script` tag in the browser while avoiding unwanted interactions with other code. As we have an export, we need to provide the name of a global variable that will be created by our bundle so that other code can access our export via this variable. + +```js +// rollup.config.js +import json from 'rollup-plugin-json'; +import {terser} from 'rollup-plugin-terser'; + +export default { + input: 'src/main.js', + output: [ + { + file: 'bundle.js', + format: 'cjs' + }, + { + file: 'bundle.min.js', + format: 'iife', + name: 'version', + plugins: [terser()] + } + ], + plugins: [ json() ] +}; +``` + +Besides `bundle.js`, Rollup will now create a second file `bundle.min.js`: + +```js +var version = (function () { + 'use strict'; + + var version = "1.0.0"; + + function main () { + console.log('version ' + version); + } + + return main; + +}()); +``` + + ### Code Splitting To use the code splitting feature, we got back to the original example and modify `src/main.js` to load `src/foo.js` dynamically instead of statically: diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index 974f6a45e41..ecb4d46eccd 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -72,9 +72,12 @@ In addition to properties defining the identity of your plugin, you may also spe * `sequential`: If this hook returns a promise, then other hooks of this kind will only be executed once this hook has resolved * `parallel`: If this hook returns a promise, then other hooks of this kind will not wait for this hook to be resolved +Furthermore, hooks can be run either during the `build` phase of the Rollup build, which is triggered by `rollup.rollup()`, or during the `generate` phase, which is triggered by `bundle.generate()` or `bundle.write()`. Plugins that only use `generate` phase hooks can also be passed in via the output options to `bundle.generate()` or `bundle.write()` and therefore run only for certain outputs. + #### `augmentChunkHash` Type: `(preRenderedChunk: PreRenderedChunk) => string`
-Kind: `sync, sequential` +Kind: `sync, sequential`
+Phase: `generate` Can be used to augment the hash of individual chunks. Called for each Rollup output chunk. Returning a falsy value will not modify the hash. @@ -91,31 +94,36 @@ augmentChunkHash(chunkInfo) { #### `banner` Type: `string | (() => string)`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Cf. [`output.banner/output.footer`](guide/en/#outputbanneroutputfooter). #### `buildEnd` Type: `(error?: Error) => void`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `build` Called when rollup has finished bundling, but before `generate` or `write` is called; you can also return a Promise. If an error occurred during the build, it is passed on to this hook. #### `buildStart` Type: `(options: InputOptions) => void`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `build` -Called on each `rollup.rollup` build. +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. #### `footer` Type: `string | (() => string)`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Cf. [`output.banner/output.footer`](guide/en/#outputbanneroutputfooter). #### `generateBundle` Type: `(options: OutputOptions, bundle: { [fileName: string]: AssetInfo | ChunkInfo }, isWrite: boolean) => void`
-Kind: `async, sequential` +Kind: `async, sequential`
+Phase: `generate` Called at the end of `bundle.generate()` or immediately before the files are written in `bundle.write()`. To modify the files after they have been written, use the [`writeBundle`](guide/en/#writebundle) hook. `bundle` provides the full list of files being written or generated along with their details: @@ -155,13 +163,15 @@ You can prevent files from being emitted by deleting them from the bundle object #### `intro` Type: `string | (() => string)`
-Kind: `async, parallel` +Kind: `async, parallel`
+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 }`
-Kind: `async, first` +Kind: `async, first`
+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 }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node. @@ -171,43 +181,52 @@ You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--m #### `options` Type: `(options: InputOptions) => InputOptions | null`
-Kind: `sync, sequential` +Kind: `sync, sequential`
+Phase: `build` + +Replaces or manipulates the options object passed to `rollup.rollup`. Returning `null` does not replace anything. If you just need to read the options, it is recommended to use the [`buildStart`](guide/en/#buildstart) hook as that hook has access to the options after the transformations from all `options` hooks have been taken into account. -Reads and replaces or manipulates the options object passed to `rollup.rollup`. Returning `null` does not replace anything. This is the only hook that does not have access to most [plugin context](guide/en/#plugin-context) utility functions as it is run before rollup is fully configured. +This is the only hook that does not have access to most [plugin context](guide/en/#plugin-context) utility functions as it is run before rollup is fully configured. #### `outputOptions` Type: `(outputOptions: OutputOptions) => OutputOptions | null`
-Kind: `sync, sequential` +Kind: `sync, sequential`
+Phase: `generate` -Reads and replaces or manipulates the output options object passed to `bundle.generate`. Returning `null` does not replace anything. +Replaces or manipulates the output options object passed to `bundle.generate()` or `bundle.write()`. Returning `null` does not replace anything. If you just need to read the output options, it is recommended to use the [`renderStart`](guide/en/#renderstart) hook as this hook has access to the output options after the transformations from all `outputOptions` hooks have been taken into account. #### `outro` Type: `string | (() => string)`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Cf. [`output.intro/output.outro`](guide/en/#outputintrooutputoutro). #### `renderChunk` Type: `(code: string, chunk: ChunkInfo, options: OutputOptions) => string | { code: string, map: SourceMap } | null`
-Kind: `async, sequential` +Kind: `async, sequential`
+Phase: `generate` Can be used to transform individual chunks. Called for each Rollup output chunk file. Returning `null` will apply no transformations. #### `renderError` Type: `(error: Error) => void`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Called when rollup encounters an error during `bundle.generate()` or `bundle.write()`. The error is passed to this hook. To get notified when generation completes successfully, use the `generateBundle` hook. #### `renderStart` -Type: `() => void`
-Kind: `async, parallel` +Type: `(outputOptions: OutputOptions, inputOptions: InputOptions) => void`
+Kind: `async, parallel`
+Phase: `generate` -Called initially each time `bundle.generate()` or `bundle.write()` is called. To get notified when generation has completed, use the `generateBundle` and `renderError` hooks. +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. #### `resolveDynamicImport` Type: `(specifier: string | ESTree.Node, importer: string) => string | false | null | {id: string, external?: boolean}`
-Kind: `async, first` +Kind: `async, first`
+Phase: `generate` Defines a custom resolver for dynamic imports. Returning `false` signals that the import should be kept as it is and not be passed to other resolvers thus making it external. Similar to the [`resolveId`](guide/en/#resolveid) hook, you can also return an object to resolve the import to a different id while marking it as external at the same time. @@ -222,7 +241,8 @@ Note that the return value of this hook will not be passed to `resolveId` afterw #### `resolveFileUrl` Type: `({chunkId: string, fileName: string, format: string, moduleId: string, referenceId: string, relativePath: string}) => string | null`
-Kind: `sync, first` +Kind: `sync, first`
+Phase: `generate` Allows to customize how Rollup resolves URLs of files that were emitted by plugins via `this.emitAsset` or `this.emitChunk`. By default, Rollup will generate code for `import.meta.ROLLUP_ASSET_URL_assetReferenceId` and `import.meta.ROLLUP_CHUNK_URL_chunkReferenceId` that should correctly generate absolute URLs of emitted files independent of the output format and the host system where the code is deployed. @@ -249,7 +269,8 @@ resolveFileUrl({fileName}) { #### `resolveId` Type: `(source: string, importer: string) => string | false | null | {id: string, external?: boolean, moduleSideEffects?: boolean | null}`
-Kind: `async, first` +Kind: `async, first`
+Phase: `build` 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 this happens for a relative import, the id will be renormalized the same way as when the `external` option is used. @@ -270,7 +291,8 @@ If `false` is returned for `moduleSideEffects` in the first hook that resolves a #### `resolveImportMeta` Type: `(property: string | null, {chunkId: string, moduleId: string, format: string}) => string | null`
-Kind: `sync, first` +Kind: `sync, first`
+Phase: `generate` Allows to customize how Rollup handles `import.meta` and `import.meta.someProperty`, in particular `import.meta.url`. In ES modules, `import.meta` is an object and `import.meta.url` contains the URL of the current module, e.g. `http://server.net/bundle.js` for browsers or `file:///path/to/bundle.js` in Node. @@ -292,7 +314,8 @@ Note that since this hook has access to the filename of the current chunk, its r #### `transform` Type: `(code: string, id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null }`
-Kind: `async, sequential` +Kind: `async, sequential`
+Phase: `build` 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. @@ -304,13 +327,15 @@ You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--m #### `watchChange` Type: `(id: string) => void`
-Kind: `sync, sequential` +Kind: `sync, sequential`
+Phase: `build` (can also be triggered during `generate` but cannot be used by output plugins) Notifies a plugin whenever rollup has detected a change to a monitored file in `--watch` mode. #### `writeBundle` Type: `( bundle: { [fileName: string]: AssetInfo | ChunkInfo }) => void`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Called only at the end of `bundle.write()` once all files have been written. Similar to the [`generateBundle`](guide/en/#generatebundle) hook, `bundle` provides the full list of files being written along with their details. diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index 2838105e143..fbf70361497 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -226,6 +226,35 @@ this.a.b.c = ... */ ``` +#### output.plugins +Type: `OutputPlugin | (OutputPlugin | void)[]` + +Adds a plugin just to this output. See [Using output plugins](guide/en/#using-output-plugins) for more information on how to use output-specific plugins and [Plugins](guide/en/#plugin-development) on how to write your own. For plugins imported from packages, remember to call the imported plugin function (i.e. `commonjs()`, not just `commonjs`). Falsy plugins will be ignored, which can be used to easily activate or deactivate plugins. + +Not every plugin can be used here. `output.plugins` is limited to plugins that only use hooks that run during `bundle.generate()` or `bundle.write()`, i.e. after Rollup's main analysis is complete. If you are a plugin author, see [Plugin hooks](guide/en/#hooks) to find out which hooks can be used. + +The following will add minifaction to one of the outputs: + +```js +// rollup.config.js +import {terser} from 'rollup-plugin-terser'; + +export default { + input: 'main.js', + output: [ + { + file: 'bundle.js', + format: 'esm' + }, + { + file: 'bundle.min.js', + format: 'esm', + plugins: [terser()] + } + ] +}; +``` + #### plugins Type: `Plugin | (Plugin | void)[]` @@ -239,12 +268,16 @@ import commonjs from 'rollup-plugin-commonjs'; const isProduction = process.env.NODE_ENV === 'production'; export default (async () => ({ - entry: 'main.js', + input: 'main.js', plugins: [ resolve(), commonjs(), isProduction && (await import('rollup-plugin-terser')).terser() - ] + ], + output: { + file: 'bundle.js', + format: 'cjs' + } }))(); ``` diff --git a/src/Chunk.ts b/src/Chunk.ts index bd077275922..b6ead70e594 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -32,6 +32,7 @@ import { sortByExecutionOrder } from './utils/executionOrder'; import getIndentString from './utils/getIndentString'; import { makeLegal } from './utils/identifierHelpers'; import { basename, dirname, extname, isAbsolute, normalize, resolve } from './utils/path'; +import { PluginDriver } from './utils/PluginDriver'; import relativeId, { getAliasName } from './utils/relativeId'; import renderChunk from './utils/renderChunk'; import { RenderOptions } from './utils/renderHelpers'; @@ -256,7 +257,8 @@ export default class Chunk { addons: Addons, options: OutputOptions, existingNames: Record, - includeHash: boolean + includeHash: boolean, + outputPluginDriver: PluginDriver ): string { if (this.fileName !== null) { return this.fileName; @@ -270,7 +272,12 @@ export default class Chunk { format: () => (options.format === 'es' ? 'esm' : (options.format as string)), hash: () => includeHash - ? this.computeContentHashWithDependencies(addons, options, existingNames) + ? this.computeContentHashWithDependencies( + addons, + options, + existingNames, + outputPluginDriver + ) : '[hash]', name: () => this.getChunkName() }), @@ -363,11 +370,11 @@ export default class Chunk { return this.dependencies.map(chunk => chunk.id).filter(Boolean) as string[]; } - getRenderedHash(): string { + getRenderedHash(outputPluginDriver: PluginDriver): string { if (this.renderedHash) return this.renderedHash; if (!this.renderedSource) return ''; const hash = createHash(); - const hashAugmentation = this.calculateHashAugmentation(); + const hashAugmentation = this.calculateHashAugmentation(outputPluginDriver); hash.update(hashAugmentation); hash.update(this.renderedSource.toString()); hash.update( @@ -629,22 +636,16 @@ export default class Chunk { timeEnd('render modules', 3); } - render(options: OutputOptions, addons: Addons, outputChunk: RenderedChunk) { + render( + options: OutputOptions, + addons: Addons, + outputChunk: RenderedChunk, + outputPluginDriver: PluginDriver + ) { timeStart('render format', 3); - if (!this.renderedSource) - throw new Error('Internal error: Chunk render called before preRender'); - const format = options.format as string; const finalise = finalisers[format]; - if (!finalise) { - error({ - code: 'INVALID_OPTION', - message: `Invalid format: ${format} - valid options are ${Object.keys(finalisers).join( - ', ' - )}.` - }); - } if (options.dynamicImportFunction && format !== 'es') { this.graph.warn({ code: 'INVALID_OPTION', @@ -665,7 +666,7 @@ export default class Chunk { } this.finaliseDynamicImports(format); - this.finaliseImportMetas(format); + this.finaliseImportMetas(format, outputPluginDriver); const hasExports = this.renderedDeclarations.exports.length !== 0 || @@ -697,7 +698,7 @@ export default class Chunk { } const magicString = finalise( - this.renderedSource, + this.renderedSource as MagicStringBundle, { accessedGlobals, dependencies: this.renderedDeclarations.dependencies, @@ -726,8 +727,8 @@ export default class Chunk { return renderChunk({ chunk: this, code: prevCode, - graph: this.graph, options, + outputPluginDriver, renderChunk: outputChunk, sourcemapChain: chunkSourcemapChain }).then((code: string) => { @@ -826,7 +827,7 @@ export default class Chunk { } } - private calculateHashAugmentation(): string { + private calculateHashAugmentation(outputPluginDriver: PluginDriver): string { const facadeModule = this.facadeModule; const getChunkName = this.getChunkName.bind(this); const preRenderedChunk = { @@ -841,7 +842,7 @@ export default class Chunk { return getChunkName(); } } as PreRenderedChunk; - const hashAugmentation = this.graph.pluginDriver.hookReduceValueSync( + return outputPluginDriver.hookReduceValueSync( 'augmentChunkHash', '', [preRenderedChunk], @@ -852,13 +853,13 @@ export default class Chunk { return hashAugmentation; } ); - return hashAugmentation; } private computeContentHashWithDependencies( addons: Addons, options: OutputOptions, - existingNames: Record + existingNames: Record, + outputPluginDriver: PluginDriver ): string { const hash = createHash(); hash.update( @@ -869,8 +870,8 @@ export default class Chunk { if (dep instanceof ExternalModule) { hash.update(':' + dep.renderPath); } else { - hash.update(dep.getRenderedHash()); - hash.update(dep.generateId(addons, options, existingNames, false)); + hash.update(dep.getRenderedHash(outputPluginDriver)); + hash.update(dep.generateId(addons, options, existingNames, false, outputPluginDriver)); } }); @@ -907,10 +908,10 @@ export default class Chunk { } } - private finaliseImportMetas(format: string): void { + private finaliseImportMetas(format: string, outputPluginDriver: PluginDriver): void { for (const [module, code] of this.renderedModuleSources) { for (const importMeta of module.importMetas) { - importMeta.renderFinalMechanism(code, this.id as string, format, this.graph.pluginDriver); + importMeta.renderFinalMechanism(code, this.id as string, format, outputPluginDriver); } } } diff --git a/src/Graph.ts b/src/Graph.ts index 0ffa5a11fed..49c6913ddfb 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -29,7 +29,7 @@ import { Uint8ArrayToHexString } from './utils/entryHashing'; import { errDeprecation, error } from './utils/error'; import { analyseModuleExecution, sortByExecutionOrder } from './utils/executionOrder'; import { resolve } from './utils/path'; -import { createPluginDriver, PluginDriver } from './utils/pluginDriver'; +import { PluginDriver } from './utils/PluginDriver'; import relativeId from './utils/relativeId'; import { timeEnd, timeStart } from './utils/timers'; @@ -143,7 +143,13 @@ export default class Graph { ...this.acornOptions }) as any; - this.pluginDriver = createPluginDriver(this, options, this.pluginCache, watcher); + this.pluginDriver = new PluginDriver( + this, + options.plugins as Plugin[], + this.pluginCache, + options.preserveSymlinks === true, + watcher + ); if (watcher) { const handleChange = (id: string) => this.pluginDriver.hookSeqSync('watchChange', [id]); diff --git a/src/Module.ts b/src/Module.ts index fbc651fd75c..11ac9fce855 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -91,7 +91,6 @@ export interface AstContext { error: (props: RollupError, pos: number) => void; fileName: string; getExports: () => string[]; - getFileName: (fileReferenceId: string) => string; getModuleExecIndex: () => number; getModuleName: () => string; getReexports: () => string[]; @@ -586,7 +585,6 @@ export default class Module { error: this.error.bind(this), fileName, // Needed for warnings getExports: this.getExports.bind(this), - getFileName: this.graph.pluginDriver.getFileName, getModuleExecIndex: () => this.execIndex, getModuleName: this.basename.bind(this), getReexports: this.getReexports.bind(this), diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index ab2a8b38983..9c6e21f8256 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -26,7 +26,7 @@ import { errUnresolvedImportTreatedAsExternal } from './utils/error'; import { isRelative, resolve } from './utils/path'; -import { PluginDriver } from './utils/pluginDriver'; +import { PluginDriver } from './utils/PluginDriver'; import relativeId from './utils/relativeId'; import { timeEnd, timeStart } from './utils/timers'; import transform from './utils/transform'; diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index 9a328513d62..bccedbddeb9 100644 --- a/src/ast/nodes/MetaProperty.ts +++ b/src/ast/nodes/MetaProperty.ts @@ -1,7 +1,7 @@ import MagicString from 'magic-string'; import { accessedFileUrlGlobals, accessedMetaUrlGlobals } from '../../utils/defaultPlugin'; import { dirname, normalize, relative } from '../../utils/path'; -import { PluginDriver } from '../../utils/pluginDriver'; +import { PluginDriver } from '../../utils/PluginDriver'; import { ObjectPathKey } from '../utils/PathTracker'; import Identifier from './Identifier'; import MemberExpression from './MemberExpression'; @@ -59,7 +59,7 @@ export default class MetaProperty extends NodeBase { code: MagicString, chunkId: string, format: string, - pluginDriver: PluginDriver + outputPluginDriver: PluginDriver ): void { if (!this.included) return; const parent = this.parent; @@ -77,26 +77,26 @@ export default class MetaProperty extends NodeBase { let fileName: string; if (metaProperty.startsWith(FILE_PREFIX)) { referenceId = metaProperty.substr(FILE_PREFIX.length); - fileName = this.context.getFileName(referenceId); + fileName = outputPluginDriver.getFileName(referenceId); } else if (metaProperty.startsWith(ASSET_PREFIX)) { this.context.warnDeprecation( `Using the "${ASSET_PREFIX}" prefix to reference files is deprecated. Use the "${FILE_PREFIX}" prefix instead.`, false ); assetReferenceId = metaProperty.substr(ASSET_PREFIX.length); - fileName = this.context.getFileName(assetReferenceId); + fileName = outputPluginDriver.getFileName(assetReferenceId); } else { this.context.warnDeprecation( `Using the "${CHUNK_PREFIX}" prefix to reference files is deprecated. Use the "${FILE_PREFIX}" prefix instead.`, false ); chunkReferenceId = metaProperty.substr(CHUNK_PREFIX.length); - fileName = this.context.getFileName(chunkReferenceId); + fileName = outputPluginDriver.getFileName(chunkReferenceId); } const relativePath = normalize(relative(dirname(chunkId), fileName)); let replacement; if (assetReferenceId !== null) { - replacement = pluginDriver.hookFirstSync('resolveAssetUrl', [ + replacement = outputPluginDriver.hookFirstSync('resolveAssetUrl', [ { assetFileName: fileName, chunkId, @@ -107,7 +107,7 @@ export default class MetaProperty extends NodeBase { ]); } if (!replacement) { - replacement = pluginDriver.hookFirstSync<'resolveFileUrl', string>('resolveFileUrl', [ + replacement = outputPluginDriver.hookFirstSync<'resolveFileUrl', string>('resolveFileUrl', [ { assetReferenceId, chunkId, @@ -130,7 +130,7 @@ export default class MetaProperty extends NodeBase { return; } - const replacement = pluginDriver.hookFirstSync('resolveImportMeta', [ + const replacement = outputPluginDriver.hookFirstSync('resolveImportMeta', [ metaProperty, { chunkId, diff --git a/src/rollup/index.ts b/src/rollup/index.ts index 2ff3c1db41a..b4503c660b3 100644 --- a/src/rollup/index.ts +++ b/src/rollup/index.ts @@ -10,7 +10,8 @@ import { writeFile } from '../utils/fs'; import getExportMode from '../utils/getExportMode'; import mergeOptions, { GenericConfigObject } from '../utils/mergeOptions'; import { basename, dirname, isAbsolute, resolve } from '../utils/path'; -import { ANONYMOUS_PLUGIN_PREFIX, PluginDriver } from '../utils/pluginDriver'; +import { PluginDriver } from '../utils/PluginDriver'; +import { ANONYMOUS_OUTPUT_PLUGIN_PREFIX, ANONYMOUS_PLUGIN_PREFIX } from '../utils/pluginUtils'; import { SOURCEMAPPING_URL } from '../utils/sourceMappingURL'; import { getTimings, initialiseTimers, timeEnd, timeStart } from '../utils/timers'; import { @@ -81,6 +82,17 @@ function ensureArray(items: (T | null | undefined)[] | T | null | undefined): return []; } +function normalizePlugins(rawPlugins: any, anonymousPrefix: string): Plugin[] { + const plugins = ensureArray(rawPlugins); + for (let pluginIndex = 0; pluginIndex < plugins.length; pluginIndex++) { + const plugin = plugins[pluginIndex]; + if (!plugin.name) { + plugin.name = `${anonymousPrefix}${pluginIndex + 1}`; + } + } + return plugins; +} + function getInputOptions(rawInputOptions: GenericConfigObject): InputOptions { if (!rawInputOptions) { throw new Error('You must supply an options object to rollup'); @@ -93,13 +105,7 @@ function getInputOptions(rawInputOptions: GenericConfigObject): InputOptions { (inputOptions.onwarn as WarningHandler)({ message: optionError, code: 'UNKNOWN_OPTION' }); inputOptions = ensureArray(inputOptions.plugins).reduce(applyOptionHook, inputOptions); - inputOptions.plugins = ensureArray(inputOptions.plugins); - for (let pluginIndex = 0; pluginIndex < inputOptions.plugins.length; pluginIndex++) { - const plugin = inputOptions.plugins[pluginIndex]; - if (!plugin.name) { - plugin.name = `${ANONYMOUS_PLUGIN_PREFIX}${pluginIndex + 1}`; - } - } + inputOptions.plugins = normalizePlugins(inputOptions.plugins, ANONYMOUS_PLUGIN_PREFIX); if (inputOptions.inlineDynamicImports) { if (inputOptions.preserveModules) @@ -214,27 +220,43 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom // ensure we only do one optimization pass per build let optimized = false; - function getOutputOptions(rawOutputOptions: GenericConfigObject) { - return normalizeOutputOptions( - inputOptions as GenericConfigObject, - rawOutputOptions, - chunks.length > 1, - graph.pluginDriver + function getOutputOptionsAndPluginDriver( + rawOutputOptions: GenericConfigObject + ): { outputOptions: OutputOptions; outputPluginDriver: PluginDriver } { + if (!rawOutputOptions) { + throw new Error('You must supply an options object'); + } + const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver( + normalizePlugins(rawOutputOptions.plugins, ANONYMOUS_OUTPUT_PLUGIN_PREFIX) ); + + return { + outputOptions: normalizeOutputOptions( + inputOptions as GenericConfigObject, + rawOutputOptions, + chunks.length > 1, + outputPluginDriver + ), + outputPluginDriver + }; } - async function generate(outputOptions: OutputOptions, isWrite: boolean): Promise { + async function generate( + outputOptions: OutputOptions, + isWrite: boolean, + outputPluginDriver: PluginDriver + ): Promise { timeStart('GENERATE', 1); const assetFileNames = outputOptions.assetFileNames || 'assets/[name]-[hash][extname]'; + const inputBase = commondir(getAbsoluteEntryModulePaths(chunks)); const outputBundleWithPlaceholders: OutputBundleWithPlaceholders = Object.create(null); + outputPluginDriver.setOutputBundle(outputBundleWithPlaceholders, assetFileNames); let outputBundle; - const inputBase = commondir(getAbsoluteEntryModulePaths(chunks)); - graph.pluginDriver.startOutput(outputBundleWithPlaceholders, assetFileNames); try { - await graph.pluginDriver.hookParallel('renderStart', []); - const addons = await createAddons(graph, outputOptions); + await outputPluginDriver.hookParallel('renderStart', [outputOptions, inputOptions]); + const addons = await createAddons(outputOptions, outputPluginDriver); for (const chunk of chunks) { if (!inputOptions.preserveModules) chunk.generateInternalExports(outputOptions); if (chunk.facadeModule && chunk.facadeModule.isEntryPoint) @@ -253,29 +275,32 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom outputOptions, inputBase, addons, - outputBundleWithPlaceholders + outputBundleWithPlaceholders, + outputPluginDriver ); outputBundle = assignChunksToBundle(chunks, outputBundleWithPlaceholders); await Promise.all( chunks.map(chunk => { const outputChunk = outputBundleWithPlaceholders[chunk.id as string] as OutputChunk; - return chunk.render(outputOptions, addons, outputChunk).then(rendered => { - outputChunk.code = rendered.code; - outputChunk.map = rendered.map; - - return graph.pluginDriver.hookParallel('ongenerate', [ - { bundle: outputChunk, ...outputOptions }, - outputChunk - ]); - }); + return chunk + .render(outputOptions, addons, outputChunk, outputPluginDriver) + .then(rendered => { + outputChunk.code = rendered.code; + outputChunk.map = rendered.map; + + return outputPluginDriver.hookParallel('ongenerate', [ + { bundle: outputChunk, ...outputOptions }, + outputChunk + ]); + }); }) ); } catch (error) { - await graph.pluginDriver.hookParallel('renderError', [error]); + await outputPluginDriver.hookParallel('renderError', [error]); throw error; } - await graph.pluginDriver.hookSeq('generateBundle', [outputOptions, outputBundle, isWrite]); + await outputPluginDriver.hookSeq('generateBundle', [outputOptions, outputBundle, isWrite]); for (const key of Object.keys(outputBundle)) { const file = outputBundle[key] as any; if (!file.type) { @@ -286,7 +311,7 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom file.type = 'asset'; } } - graph.pluginDriver.finaliseAssets(); + outputPluginDriver.finaliseAssets(); timeEnd('GENERATE', 1); return outputBundle; @@ -296,7 +321,10 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom const result: RollupBuild = { cache: cache as RollupCache, generate: ((rawOutputOptions: GenericConfigObject) => { - const promise = generate(getOutputOptions(rawOutputOptions), false).then(result => + const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver( + rawOutputOptions + ); + const promise = generate(outputOptions, false, outputPluginDriver).then(result => createOutput(result) ); Object.defineProperty(promise, 'code', throwAsyncGenerateError); @@ -305,22 +333,24 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom }) as any, watchFiles: Object.keys(graph.watchFiles), write: ((rawOutputOptions: GenericConfigObject) => { - const outputOptions = getOutputOptions(rawOutputOptions); + const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver( + rawOutputOptions + ); if (!outputOptions.dir && !outputOptions.file) { error({ code: 'MISSING_OPTION', message: 'You must specify "output.file" or "output.dir" for the build.' }); } - return generate(outputOptions, true).then(async bundle => { - let chunkCnt = 0; + return generate(outputOptions, true, outputPluginDriver).then(async bundle => { + let chunkCount = 0; for (const fileName of Object.keys(bundle)) { const file = bundle[fileName]; if (file.type === 'asset') continue; - chunkCnt++; - if (chunkCnt > 1) break; + chunkCount++; + if (chunkCount > 1) break; } - if (chunkCnt > 1) { + if (chunkCount > 1) { if (outputOptions.sourcemapFile) error({ code: 'INVALID_OPTION', @@ -339,10 +369,10 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom } await Promise.all( Object.keys(bundle).map(chunkId => - writeOutputFile(graph, result, bundle[chunkId], outputOptions) + writeOutputFile(result, bundle[chunkId], outputOptions, outputPluginDriver) ) ); - await graph.pluginDriver.hookParallel('writeBundle', [bundle]); + await outputPluginDriver.hookParallel('writeBundle', [bundle]); return createOutput(bundle); }); }) as any @@ -383,10 +413,10 @@ function createOutput(outputBundle: Record { const fileName = resolve( outputOptions.dir || dirname(outputOptions.file as string), @@ -417,7 +447,7 @@ function writeOutputFile( .then( (): any => outputFile.type === 'chunk' && - graph.pluginDriver.hookSeq('onwrite', [ + outputPluginDriver.hookSeq('onwrite', [ { bundle: build, ...outputOptions @@ -432,11 +462,8 @@ function normalizeOutputOptions( inputOptions: GenericConfigObject, rawOutputOptions: GenericConfigObject, hasMultipleChunks: boolean, - pluginDriver: PluginDriver + outputPluginDriver: PluginDriver ): OutputOptions { - if (!rawOutputOptions) { - throw new Error('You must supply an options object'); - } const mergedOptions = mergeOptions({ config: { output: { @@ -453,7 +480,7 @@ function normalizeOutputOptions( const mergedOutputOptions = mergedOptions.outputOptions[0]; const outputOptionsReducer = (outputOptions: OutputOptions, result: OutputOptions) => result || outputOptions; - const outputOptions = pluginDriver.hookReduceArg0Sync( + const outputOptions = outputPluginDriver.hookReduceArg0Sync( 'outputOptions', [mergedOutputOptions], outputOptionsReducer, diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 32c3e5e237c..343491308f0 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -332,17 +332,14 @@ interface OnWriteOptions extends OutputOptions { bundle: RollupBuild; } -export interface PluginHooks { +interface OutputPluginHooks { augmentChunkHash: (this: PluginContext, chunk: PreRenderedChunk) => string | void; - buildEnd: (this: PluginContext, err?: Error) => Promise | void; - buildStart: (this: PluginContext, options: InputOptions) => Promise | void; generateBundle: ( this: PluginContext, options: OutputOptions, bundle: OutputBundle, isWrite: boolean ) => void | Promise; - load: LoadHook; /** @deprecated Use `generateBundle` instead */ ongenerate: ( this: PluginContext, @@ -355,33 +352,50 @@ export interface PluginHooks { options: OnWriteOptions, chunk: OutputChunk ) => void | Promise; - options: (this: MinimalPluginContext, options: InputOptions) => InputOptions | null | undefined; outputOptions: (this: PluginContext, options: OutputOptions) => OutputOptions | null | undefined; renderChunk: RenderChunkHook; renderError: (this: PluginContext, err?: Error) => Promise | void; - renderStart: (this: PluginContext) => Promise | void; + renderStart: ( + this: PluginContext, + outputOptions: OutputOptions, + inputOptions: InputOptions + ) => Promise | void; /** @deprecated Use `resolveFileUrl` instead */ resolveAssetUrl: ResolveAssetUrlHook; resolveDynamicImport: ResolveDynamicImportHook; resolveFileUrl: ResolveFileUrlHook; - resolveId: ResolveIdHook; - resolveImportMeta: ResolveImportMetaHook; - transform: TransformHook; /** @deprecated Use `renderChunk` instead */ transformBundle: TransformChunkHook; /** @deprecated Use `renderChunk` instead */ transformChunk: TransformChunkHook; - watchChange: (id: string) => void; writeBundle: (this: PluginContext, bundle: OutputBundle) => void | Promise; } -export interface Plugin extends Partial { - banner?: AddonHook; - cacheKey?: string; - footer?: AddonHook; - intro?: AddonHook; +export interface PluginHooks extends OutputPluginHooks { + buildEnd: (this: PluginContext, err?: Error) => Promise | void; + buildStart: (this: PluginContext, options: InputOptions) => Promise | void; + load: LoadHook; + options: (this: MinimalPluginContext, options: InputOptions) => InputOptions | null | undefined; + resolveId: ResolveIdHook; + resolveImportMeta: ResolveImportMetaHook; + transform: TransformHook; + watchChange: (id: string) => void; +} + +interface OutputPluginValueHooks { + banner: AddonHook; + cacheKey: string; + footer: AddonHook; + intro: AddonHook; + outro: AddonHook; +} + +export interface Plugin extends Partial, Partial { + name: string; +} + +export interface OutputPlugin extends Partial, Partial { name: string; - outro?: AddonHook; } export interface TreeshakingOptions { @@ -475,6 +489,7 @@ export interface OutputOptions { noConflict?: boolean; outro?: string | (() => string | Promise); paths?: OptionsPaths; + plugins?: OutputPlugin[]; preferConst?: boolean; sourcemap?: boolean | 'inline' | 'hidden'; sourcemapExcludeSources?: boolean; @@ -553,6 +568,7 @@ export interface RollupBuild { } export interface RollupOptions extends InputOptions { + // This is included for compatibility with config files but ignored by rollup.rollup output?: OutputOptions | OutputOptions[]; } diff --git a/src/utils/FileEmitter.ts b/src/utils/FileEmitter.ts index 5ddb3b14e78..88e34a8bb3d 100644 --- a/src/utils/FileEmitter.ts +++ b/src/utils/FileEmitter.ts @@ -10,6 +10,7 @@ import { errAssetSourceAlreadySet, errChunkNotGeneratedForFileName, errFailedValidation, + errFileNameConflict, errFileReferenceIdNotFoundForFilename, errInvalidRollupPhaseForChunkEmission, errNoAssetSourceSet, @@ -47,9 +48,14 @@ function generateAssetFileName( ); } -function reserveFileNameInBundle(fileName: string, bundle: OutputBundleWithPlaceholders) { - // TODO this should warn if the fileName is already in the bundle, - // but until #3174 is fixed, this raises spurious warnings and is disabled +function reserveFileNameInBundle( + fileName: string, + bundle: OutputBundleWithPlaceholders, + graph: Graph +) { + if (fileName in bundle) { + graph.warn(errFileNameConflict(fileName)); + } bundle[fileName] = FILE_PLACEHOLDER; } @@ -132,16 +138,24 @@ function getChunkFileName(file: ConsumedChunk): string { } export class FileEmitter { - private filesByReferenceId = new Map(); - // tslint:disable member-ordering - private buildFilesByReferenceId = this.filesByReferenceId; + private filesByReferenceId: Map; private graph: Graph; private output: OutputSpecificFileData | null = null; - constructor(graph: Graph) { + constructor(graph: Graph, baseFileEmitter?: FileEmitter) { this.graph = graph; + this.filesByReferenceId = baseFileEmitter + ? new Map(baseFileEmitter.filesByReferenceId) + : new Map(); } + public assertAssetsFinalized = (): void => { + for (const [referenceId, emittedFile] of this.filesByReferenceId.entries()) { + if (emittedFile.type === 'asset' && typeof emittedFile.fileName !== 'string') + error(errNoAssetSourceSet(emittedFile.name || referenceId)); + } + }; + public emitFile = (emittedFile: unknown): string => { if (!hasValidType(emittedFile)) { return error( @@ -166,7 +180,7 @@ export class FileEmitter { } }; - public getFileName = (fileReferenceId: string) => { + public getFileName = (fileReferenceId: string): string => { const emittedFile = this.filesByReferenceId.get(fileReferenceId); if (!emittedFile) return error(errFileReferenceIdNotFoundForFilename(fileReferenceId)); if (emittedFile.type === 'chunk') { @@ -176,7 +190,7 @@ export class FileEmitter { } }; - public setAssetSource = (referenceId: string, requestedSource: unknown) => { + public setAssetSource = (referenceId: string, requestedSource: unknown): void => { const consumedFile = this.filesByReferenceId.get(referenceId); if (!consumedFile) return error(errAssetReferenceIdNotFoundForSetSource(referenceId)); if (consumedFile.type !== 'asset') { @@ -197,15 +211,17 @@ export class FileEmitter { } }; - public startOutput(outputBundle: OutputBundleWithPlaceholders, assetFileNames: string) { - this.filesByReferenceId = new Map(this.buildFilesByReferenceId); + public setOutputBundle = ( + outputBundle: OutputBundleWithPlaceholders, + assetFileNames: string + ): void => { this.output = { assetFileNames, bundle: outputBundle }; for (const emittedFile of this.filesByReferenceId.values()) { if (emittedFile.fileName) { - reserveFileNameInBundle(emittedFile.fileName, this.output.bundle); + reserveFileNameInBundle(emittedFile.fileName, this.output.bundle, this.graph); } } for (const [referenceId, consumedFile] of this.filesByReferenceId.entries()) { @@ -213,13 +229,21 @@ export class FileEmitter { this.finalizeAsset(consumedFile, consumedFile.source, referenceId, this.output); } } - } + }; - public assertAssetsFinalized() { - for (const [referenceId, emittedFile] of this.filesByReferenceId.entries()) { - if (emittedFile.type === 'asset' && typeof emittedFile.fileName !== 'string') - error(errNoAssetSourceSet(emittedFile.name || referenceId)); - } + private assignReferenceId(file: ConsumedFile, idBase: string): string { + let referenceId: string | undefined; + do { + const hash = createHash(); + if (referenceId) { + hash.update(referenceId); + } else { + hash.update(idBase); + } + referenceId = hash.digest('hex').substr(0, 8); + } while (this.filesByReferenceId.has(referenceId)); + this.filesByReferenceId.set(referenceId, file); + return referenceId; } private emitAsset(emittedAsset: EmittedFile): string { @@ -239,7 +263,7 @@ export class FileEmitter { ); if (this.output) { if (emittedAsset.fileName) { - reserveFileNameInBundle(emittedAsset.fileName, this.output.bundle); + reserveFileNameInBundle(emittedAsset.fileName, this.output.bundle, this.graph); } if (source !== undefined) { this.finalizeAsset(consumedAsset, source, referenceId, this.output); @@ -287,27 +311,12 @@ export class FileEmitter { return this.assignReferenceId(consumedChunk, emittedChunk.id); } - private assignReferenceId(file: ConsumedFile, idBase: string): string { - let referenceId: string | undefined; - do { - const hash = createHash(); - if (referenceId) { - hash.update(referenceId); - } else { - hash.update(idBase); - } - referenceId = hash.digest('hex').substr(0, 8); - } while (this.filesByReferenceId.has(referenceId)); - this.filesByReferenceId.set(referenceId, file); - return referenceId; - } - private finalizeAsset( consumedFile: ConsumedFile, source: string | Buffer, referenceId: string, output: OutputSpecificFileData - ) { + ): void { const fileName = consumedFile.fileName || this.findExistingAssetFileNameWithSource(output.bundle, source) || diff --git a/src/utils/PluginCache.ts b/src/utils/PluginCache.ts new file mode 100644 index 00000000000..54feb3cbcf6 --- /dev/null +++ b/src/utils/PluginCache.ts @@ -0,0 +1,98 @@ +import { PluginCache, SerializablePluginCache } from '../rollup/types'; +import { error } from './error'; +import { ANONYMOUS_OUTPUT_PLUGIN_PREFIX, ANONYMOUS_PLUGIN_PREFIX } from './pluginUtils'; + +export function createPluginCache(cache: SerializablePluginCache): PluginCache { + return { + has(id: string) { + const item = cache[id]; + if (!item) return false; + item[0] = 0; + return true; + }, + get(id: string) { + const item = cache[id]; + if (!item) return undefined; + item[0] = 0; + return item[1]; + }, + set(id: string, value: any) { + cache[id] = [0, value]; + }, + delete(id: string) { + return delete cache[id]; + } + }; +} + +export function getTrackedPluginCache(pluginCache: PluginCache) { + const trackedCache = { + cache: { + has(id: string) { + trackedCache.used = true; + return pluginCache.has(id); + }, + get(id: string) { + trackedCache.used = true; + return pluginCache.get(id); + }, + set(id: string, value: any) { + trackedCache.used = true; + return pluginCache.set(id, value); + }, + delete(id: string) { + trackedCache.used = true; + return pluginCache.delete(id); + } + }, + used: false + }; + return trackedCache; +} + +export const NO_CACHE: PluginCache = { + has() { + return false; + }, + get() { + return undefined as any; + }, + set() {}, + delete() { + return false; + } +}; + +function uncacheablePluginError(pluginName: string): never { + if ( + pluginName.startsWith(ANONYMOUS_PLUGIN_PREFIX) || + pluginName.startsWith(ANONYMOUS_OUTPUT_PLUGIN_PREFIX) + ) { + return error({ + code: 'ANONYMOUS_PLUGIN_CACHE', + message: + 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.' + }); + } + return error({ + code: 'DUPLICATE_PLUGIN_NAME', + message: `The plugin name ${pluginName} is being used twice in the same build. Plugin names must be distinct or provide a cacheKey (please post an issue to the plugin if you are a plugin user).` + }); +} + +export function getCacheForUncacheablePlugin(pluginName: string): PluginCache { + return { + has() { + return uncacheablePluginError(pluginName); + }, + get() { + return uncacheablePluginError(pluginName); + }, + set() { + return uncacheablePluginError(pluginName); + }, + delete() { + return uncacheablePluginError(pluginName); + } + }; +} diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts new file mode 100644 index 00000000000..62bbdf64191 --- /dev/null +++ b/src/utils/PluginContext.ts @@ -0,0 +1,210 @@ +import { EventEmitter } from 'events'; +import { version as rollupVersion } from 'package.json'; +import ExternalModule from '../ExternalModule'; +import Graph from '../Graph'; +import Module from '../Module'; +import { + Plugin, + PluginCache, + PluginContext, + RollupWarning, + RollupWatcher, + SerializablePluginCache +} from '../rollup/types'; +import { BuildPhase } from './buildPhase'; +import { errInvalidRollupPhaseForAddWatchFile } from './error'; +import { FileEmitter } from './FileEmitter'; +import { createPluginCache, getCacheForUncacheablePlugin, NO_CACHE } from './PluginCache'; +import { + ANONYMOUS_OUTPUT_PLUGIN_PREFIX, + ANONYMOUS_PLUGIN_PREFIX, + throwPluginError +} from './pluginUtils'; + +function getDeprecatedContextHandler( + handler: H, + handlerName: string, + newHandlerName: string, + pluginName: string, + activeDeprecation: boolean, + graph: Graph +): H { + let deprecationWarningShown = false; + return (((...args: any[]) => { + if (!deprecationWarningShown) { + deprecationWarningShown = true; + graph.warnDeprecation( + { + message: `The "this.${handlerName}" plugin context function used by plugin ${pluginName} is deprecated. The "this.${newHandlerName}" plugin context function should be used instead.`, + plugin: pluginName + }, + activeDeprecation + ); + } + return handler(...args); + }) as unknown) as H; +} + +export function getPluginContexts( + pluginCache: Record | void, + graph: Graph, + fileEmitter: FileEmitter, + watcher: RollupWatcher | undefined +): (plugin: Plugin, pluginIndex: number) => PluginContext { + const existingPluginNames = new Set(); + return (plugin, pidx) => { + let cacheable = true; + if (typeof plugin.cacheKey !== 'string') { + if ( + plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) || + plugin.name.startsWith(ANONYMOUS_OUTPUT_PLUGIN_PREFIX) || + existingPluginNames.has(plugin.name) + ) { + cacheable = false; + } else { + existingPluginNames.add(plugin.name); + } + } + + let cacheInstance: PluginCache; + if (!pluginCache) { + cacheInstance = NO_CACHE; + } else if (cacheable) { + const cacheKey = plugin.cacheKey || plugin.name; + cacheInstance = createPluginCache( + pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null)) + ); + } else { + cacheInstance = getCacheForUncacheablePlugin(plugin.name); + } + + const context: PluginContext = { + addWatchFile(id) { + if (graph.phase >= BuildPhase.GENERATE) this.error(errInvalidRollupPhaseForAddWatchFile()); + graph.watchFiles[id] = true; + }, + cache: cacheInstance, + emitAsset: getDeprecatedContextHandler( + (name: string, source?: string | Buffer) => + fileEmitter.emitFile({ type: 'asset', name, source }), + 'emitAsset', + 'emitFile', + plugin.name, + false, + graph + ), + emitChunk: getDeprecatedContextHandler( + (id: string, options?: { name?: string }) => + fileEmitter.emitFile({ type: 'chunk', id, name: options && options.name }), + 'emitChunk', + 'emitFile', + plugin.name, + false, + graph + ), + emitFile: fileEmitter.emitFile, + error(err): never { + return throwPluginError(err, plugin.name); + }, + getAssetFileName: getDeprecatedContextHandler( + fileEmitter.getFileName, + 'getAssetFileName', + 'getFileName', + plugin.name, + false, + graph + ), + getChunkFileName: getDeprecatedContextHandler( + fileEmitter.getFileName, + 'getChunkFileName', + 'getFileName', + plugin.name, + false, + graph + ), + getFileName: fileEmitter.getFileName, + getModuleInfo(moduleId) { + const foundModule = graph.moduleById.get(moduleId); + if (foundModule == null) { + throw new Error(`Unable to find module ${moduleId}`); + } + + return { + hasModuleSideEffects: foundModule.moduleSideEffects, + id: foundModule.id, + importedIds: + foundModule instanceof ExternalModule + ? [] + : Array.from(foundModule.sources).map(id => foundModule.resolvedIds[id].id), + isEntry: foundModule instanceof Module && foundModule.isEntryPoint, + isExternal: foundModule instanceof ExternalModule + }; + }, + isExternal: getDeprecatedContextHandler( + (id: string, parentId: string, isResolved = false) => + graph.moduleLoader.isExternal(id, parentId, isResolved), + 'isExternal', + 'resolve', + plugin.name, + false, + graph + ), + meta: { + rollupVersion + }, + get moduleIds() { + return graph.moduleById.keys(); + }, + parse: graph.contextParse, + resolve(source, importer, options?: { skipSelf: boolean }) { + return graph.moduleLoader.resolveId( + source, + importer, + options && options.skipSelf ? pidx : null + ); + }, + resolveId: getDeprecatedContextHandler( + (source: string, importer: string) => + graph.moduleLoader + .resolveId(source, importer) + .then(resolveId => resolveId && resolveId.id), + 'resolveId', + 'resolve', + plugin.name, + false, + graph + ), + setAssetSource: fileEmitter.setAssetSource, + warn(warning) { + if (typeof warning === 'string') warning = { message: warning } as RollupWarning; + if (warning.code) warning.pluginCode = warning.code; + warning.code = 'PLUGIN_WARNING'; + warning.plugin = plugin.name; + graph.warn(warning); + }, + watcher: watcher + ? (() => { + let deprecationWarningShown = false; + + function deprecatedWatchListener(event: string, handler: () => void): EventEmitter { + if (!deprecationWarningShown) { + context.warn({ + code: 'PLUGIN_WATCHER_DEPRECATED', + message: `this.watcher usage is deprecated in plugins. Use the watchChange plugin hook and this.addWatchFile() instead.` + }); + deprecationWarningShown = true; + } + return (watcher as RollupWatcher).on(event, handler); + } + + return { + ...(watcher as EventEmitter), + addListener: deprecatedWatchListener, + on: deprecatedWatchListener + }; + })() + : (undefined as any) + }; + return context; + }; +} diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts new file mode 100644 index 00000000000..fc080e7f3e8 --- /dev/null +++ b/src/utils/PluginDriver.ts @@ -0,0 +1,287 @@ +import Graph from '../Graph'; +import { + EmitFile, + OutputBundleWithPlaceholders, + Plugin, + PluginContext, + PluginHooks, + RollupWatcher, + SerializablePluginCache +} from '../rollup/types'; +import { getRollupDefaultPlugin } from './defaultPlugin'; +import { errInputHookInOutputPlugin, error } from './error'; +import { FileEmitter } from './FileEmitter'; +import { getPluginContexts } from './PluginContext'; +import { throwPluginError, warnDeprecatedHooks } from './pluginUtils'; + +type Args = T extends (...args: infer K) => any ? K : never; +type EnsurePromise = Promise ? K : T>; + +export type Reduce = (reduction: T, result: R, plugin: Plugin) => T; +export type ReplaceContext = (context: PluginContext, plugin: Plugin) => PluginContext; + +export class PluginDriver { + public emitFile: EmitFile; + public finaliseAssets: () => void; + public getFileName: (fileReferenceId: string) => string; + public setOutputBundle: ( + outputBundle: OutputBundleWithPlaceholders, + assetFileNames: string + ) => void; + + private fileEmitter: FileEmitter; + private graph: Graph; + private pluginCache: Record | undefined; + private pluginContexts: PluginContext[]; + private plugins: Plugin[]; + private preserveSymlinks: boolean; + private previousHooks = new Set(['options']); + private watcher: RollupWatcher | undefined; + + constructor( + graph: Graph, + userPlugins: Plugin[], + pluginCache: Record | undefined, + preserveSymlinks: boolean, + watcher: RollupWatcher | undefined, + basePluginDriver?: PluginDriver + ) { + warnDeprecatedHooks(userPlugins, graph); + this.graph = graph; + this.pluginCache = pluginCache; + this.preserveSymlinks = preserveSymlinks; + this.watcher = watcher; + this.fileEmitter = new FileEmitter(graph, basePluginDriver && basePluginDriver.fileEmitter); + this.emitFile = this.fileEmitter.emitFile; + this.getFileName = this.fileEmitter.getFileName; + this.finaliseAssets = this.fileEmitter.assertAssetsFinalized; + this.setOutputBundle = this.fileEmitter.setOutputBundle; + this.plugins = userPlugins.concat( + basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)] + ); + this.pluginContexts = this.plugins.map( + getPluginContexts(pluginCache, graph, this.fileEmitter, watcher) + ); + if (basePluginDriver) { + for (const plugin of userPlugins) { + for (const hook of basePluginDriver.previousHooks) { + if (hook in plugin) { + graph.warn(errInputHookInOutputPlugin(plugin.name, hook)); + } + } + } + } + } + + public createOutputPluginDriver(plugins: Plugin[]): PluginDriver { + return new PluginDriver( + this.graph, + plugins, + this.pluginCache, + this.preserveSymlinks, + this.watcher, + this + ); + } + + // chains, first non-null result stops and returns + hookFirst>( + hookName: H, + args: Args, + replaceContext?: ReplaceContext | null, + skip?: number | null + ): EnsurePromise { + let promise: Promise = Promise.resolve(); + for (let i = 0; i < this.plugins.length; i++) { + if (skip === i) continue; + promise = promise.then((result: any) => { + if (result != null) return result; + return this.runHook(hookName, args as any[], i, false, replaceContext); + }); + } + return promise; + } + + // chains synchronously, first non-null result stops and returns + hookFirstSync>( + hookName: H, + args: Args, + replaceContext?: ReplaceContext + ): R { + for (let i = 0; i < this.plugins.length; i++) { + const result = this.runHookSync(hookName, args, i, replaceContext); + if (result != null) return result as any; + } + return null as any; + } + + // parallel, ignores returns + hookParallel( + hookName: H, + args: Args, + replaceContext?: ReplaceContext + ): Promise { + const promises: Promise[] = []; + for (let i = 0; i < this.plugins.length; i++) { + const hookPromise = this.runHook(hookName, args as any[], i, false, replaceContext); + if (!hookPromise) continue; + promises.push(hookPromise); + } + return Promise.all(promises).then(() => {}); + } + + // chains, reduces returns of type R, to type T, handling the reduced value as the first hook argument + hookReduceArg0>( + hookName: H, + [arg0, ...args]: any[], + reduce: Reduce, + replaceContext?: ReplaceContext + ) { + let promise = Promise.resolve(arg0); + for (let i = 0; i < this.plugins.length; i++) { + promise = promise.then(arg0 => { + const hookPromise = this.runHook(hookName, [arg0, ...args], i, false, replaceContext); + if (!hookPromise) return arg0; + return hookPromise.then((result: any) => + reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i]) + ); + }); + } + return promise; + } + + // chains synchronously, reduces returns of type R, to type T, handling the reduced value as the first hook argument + hookReduceArg0Sync>( + hookName: H, + [arg0, ...args]: any[], + reduce: Reduce, + replaceContext?: ReplaceContext + ): R { + for (let i = 0; i < this.plugins.length; i++) { + const result: any = this.runHookSync(hookName, [arg0, ...args], i, replaceContext); + arg0 = reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i]); + } + return arg0; + } + + // chains, reduces returns of type R, to type T, handling the reduced value separately. permits hooks as values. + hookReduceValue( + hookName: H, + initialValue: T | Promise, + args: any[], + reduce: Reduce, + replaceContext?: ReplaceContext + ): Promise { + let promise = Promise.resolve(initialValue); + for (let i = 0; i < this.plugins.length; i++) { + promise = promise.then(value => { + const hookPromise = this.runHook(hookName, args, i, true, replaceContext); + if (!hookPromise) return value; + return hookPromise.then((result: any) => + reduce.call(this.pluginContexts[i], value, result, this.plugins[i]) + ); + }); + } + return promise; + } + + // chains, reduces returns of type R, to type T, handling the reduced value separately. permits hooks as values. + hookReduceValueSync( + hookName: H, + initialValue: T, + args: any[], + reduce: Reduce, + replaceContext?: ReplaceContext + ): T { + let acc = initialValue; + for (let i = 0; i < this.plugins.length; i++) { + const result: any = this.runHookSync(hookName, args, i, replaceContext); + acc = reduce.call(this.pluginContexts[i], acc, result, this.plugins[i]); + } + return acc; + } + + // chains, ignores returns + async hookSeq( + hookName: H, + args: Args, + replaceContext?: ReplaceContext + ): Promise { + let promise: Promise = Promise.resolve() as any; + for (let i = 0; i < this.plugins.length; i++) + promise = promise.then(() => + this.runHook(hookName, args as any[], i, false, replaceContext) + ); + return promise; + } + + // chains, ignores returns + hookSeqSync( + hookName: H, + args: Args, + replaceContext?: ReplaceContext + ): void { + for (let i = 0; i < this.plugins.length; i++) + this.runHookSync(hookName, args as any[], i, replaceContext); + } + + private runHook( + hookName: string, + args: any[], + pluginIndex: number, + permitValues: boolean, + hookContext?: ReplaceContext | null + ): Promise { + this.previousHooks.add(hookName); + const plugin = this.plugins[pluginIndex]; + const hook = (plugin as any)[hookName]; + if (!hook) return undefined as any; + + let context = this.pluginContexts[pluginIndex]; + if (hookContext) { + context = hookContext(context, plugin); + } + return Promise.resolve() + .then(() => { + // permit values allows values to be returned instead of a functional hook + if (typeof hook !== 'function') { + if (permitValues) return hook; + error({ + code: 'INVALID_PLUGIN_HOOK', + message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.` + }); + } + return hook.apply(context, args); + }) + .catch(err => throwPluginError(err, plugin.name, { hook: hookName })); + } + + private runHookSync( + hookName: string, + args: any[], + pluginIndex: number, + hookContext?: ReplaceContext + ): T { + this.previousHooks.add(hookName); + const plugin = this.plugins[pluginIndex]; + let context = this.pluginContexts[pluginIndex]; + const hook = (plugin as any)[hookName]; + if (!hook) return undefined as any; + + if (hookContext) { + context = hookContext(context, plugin); + } + try { + // permit values allows values to be returned instead of a functional hook + if (typeof hook !== 'function') { + error({ + code: 'INVALID_PLUGIN_HOOK', + message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.` + }); + } + return hook.apply(context, args); + } catch (err) { + return throwPluginError(err, plugin.name, { hook: hookName }); + } + } +} diff --git a/src/utils/addons.ts b/src/utils/addons.ts index 80b7a0f5970..890175c524e 100644 --- a/src/utils/addons.ts +++ b/src/utils/addons.ts @@ -1,6 +1,6 @@ -import Graph from '../Graph'; import { OutputOptions } from '../rollup/types'; import { error } from './error'; +import { PluginDriver } from './PluginDriver'; export interface Addons { banner?: string; @@ -25,13 +25,15 @@ function evalIfFn( const concatSep = (out: string, next: string) => (next ? `${out}\n${next}` : out); const concatDblSep = (out: string, next: string) => (next ? `${out}\n\n${next}` : out); -export function createAddons(graph: Graph, options: OutputOptions): Promise { - const pluginDriver = graph.pluginDriver; +export function createAddons( + options: OutputOptions, + outputPluginDriver: PluginDriver +): Promise { return Promise.all([ - pluginDriver.hookReduceValue('banner', evalIfFn(options.banner), [], concatSep), - pluginDriver.hookReduceValue('footer', evalIfFn(options.footer), [], concatSep), - pluginDriver.hookReduceValue('intro', evalIfFn(options.intro), [], concatDblSep), - pluginDriver.hookReduceValue('outro', evalIfFn(options.outro), [], concatDblSep) + outputPluginDriver.hookReduceValue('banner', evalIfFn(options.banner), [], concatSep), + outputPluginDriver.hookReduceValue('footer', evalIfFn(options.footer), [], concatSep), + outputPluginDriver.hookReduceValue('intro', evalIfFn(options.intro), [], concatDblSep), + outputPluginDriver.hookReduceValue('outro', evalIfFn(options.outro), [], concatDblSep) ]) .then(([banner, footer, intro, outro]) => { if (intro) intro += '\n\n'; diff --git a/src/utils/assignChunkIds.ts b/src/utils/assignChunkIds.ts index ae479ae4684..48dd39917e8 100644 --- a/src/utils/assignChunkIds.ts +++ b/src/utils/assignChunkIds.ts @@ -3,6 +3,7 @@ import { InputOptions, OutputBundleWithPlaceholders, OutputOptions } from '../ro import { Addons } from './addons'; import { FILE_PLACEHOLDER } from './FileEmitter'; import { basename } from './path'; +import { PluginDriver } from './PluginDriver'; export function assignChunkIds( chunks: Chunk[], @@ -10,7 +11,8 @@ export function assignChunkIds( outputOptions: OutputOptions, inputBase: string, addons: Addons, - bundle: OutputBundleWithPlaceholders + bundle: OutputBundleWithPlaceholders, + outputPluginDriver: PluginDriver ) { const entryChunks: Chunk[] = []; const otherChunks: Chunk[] = []; @@ -29,7 +31,7 @@ export function assignChunkIds( } else if (inputOptions.preserveModules) { chunk.id = chunk.generateIdPreserveModules(inputBase, outputOptions, bundle); } else { - chunk.id = chunk.generateId(addons, outputOptions, bundle, true); + chunk.id = chunk.generateId(addons, outputOptions, bundle, true, outputPluginDriver); } bundle[chunk.id] = FILE_PLACEHOLDER; } diff --git a/src/utils/error.ts b/src/utils/error.ts index 7010eb883c2..351436f487d 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -42,6 +42,7 @@ export enum Errors { DEPRECATED_FEATURE = 'DEPRECATED_FEATURE', FILE_NOT_FOUND = 'FILE_NOT_FOUND', FILE_NAME_CONFLICT = 'FILE_NAME_CONFLICT', + INPUT_HOOK_IN_OUTPUT_PLUGIN = 'INPUT_HOOK_IN_OUTPUT_PLUGIN', INVALID_CHUNK = 'INVALID_CHUNK', INVALID_EXTERNAL_ID = 'INVALID_EXTERNAL_ID', INVALID_OPTION = 'INVALID_OPTION', @@ -122,7 +123,14 @@ export function errFileReferenceIdNotFoundForFilename(assetReferenceId: string) export function errFileNameConflict(fileName: string) { return { code: Errors.FILE_NAME_CONFLICT, - message: `Could not emit file "${fileName}" as it conflicts with an already emitted file.` + message: `The emitted file "${fileName}" overwrites a previously emitted file of the same name.` + }; +} + +export function errInputHookInOutputPlugin(pluginName: string, hookName: string) { + return { + code: Errors.INPUT_HOOK_IN_OUTPUT_PLUGIN, + message: `The "${hookName}" hook used by the output plugin ${pluginName} is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.` }; } diff --git a/src/utils/mergeOptions.ts b/src/utils/mergeOptions.ts index 4d8b4a92d95..d6332a16f77 100644 --- a/src/utils/mergeOptions.ts +++ b/src/utils/mergeOptions.ts @@ -168,14 +168,15 @@ function addUnknownOptionErrors( optionType: string, ignoredKeys: RegExp = /$./ ) { - const unknownOptions = options.filter( - key => validOptions.indexOf(key) === -1 && !ignoredKeys.test(key) - ); + const validOptionSet = new Set(validOptions); + const unknownOptions = options.filter(key => !validOptionSet.has(key) && !ignoredKeys.test(key)); if (unknownOptions.length > 0) errors.push( - `Unknown ${optionType}: ${unknownOptions.join( - ', ' - )}. Allowed options: ${validOptions.sort().join(', ')}` + `Unknown ${optionType}: ${unknownOptions.join(', ')}. Allowed options: ${Array.from( + validOptionSet + ) + .sort() + .join(', ')}` ); } @@ -283,6 +284,7 @@ function getOutputOptions( noConflict: getOption('noConflict'), outro: getOption('outro'), paths: getOption('paths'), + plugins: config.plugins as any, preferConst: getOption('preferConst'), sourcemap: getOption('sourcemap'), sourcemapExcludeSources: getOption('sourcemapExcludeSources'), diff --git a/src/utils/pluginDriver.ts b/src/utils/pluginDriver.ts deleted file mode 100644 index e9e3b10260e..00000000000 --- a/src/utils/pluginDriver.ts +++ /dev/null @@ -1,583 +0,0 @@ -import { EventEmitter } from 'events'; -import { version as rollupVersion } from 'package.json'; -import ExternalModule from '../ExternalModule'; -import Graph from '../Graph'; -import Module from '../Module'; -import { - EmitFile, - InputOptions, - OutputBundleWithPlaceholders, - Plugin, - PluginCache, - PluginContext, - PluginHooks, - RollupError, - RollupWarning, - RollupWatcher, - SerializablePluginCache -} from '../rollup/types'; -import { BuildPhase } from './buildPhase'; -import { getRollupDefaultPlugin } from './defaultPlugin'; -import { errInvalidRollupPhaseForAddWatchFile, error, Errors } from './error'; -import { FileEmitter } from './FileEmitter'; - -type Args = T extends (...args: infer K) => any ? K : never; -type EnsurePromise = Promise ? K : T>; - -export interface PluginDriver { - emitFile: EmitFile; - finaliseAssets(): void; - getFileName(assetReferenceId: string): string; - hookFirst>( - hook: H, - args: Args, - hookContext?: HookContext | null, - skip?: number | null - ): EnsurePromise; - hookFirstSync>( - hook: H, - args: Args, - hookContext?: HookContext - ): R; - hookParallel( - hook: H, - args: Args, - hookContext?: HookContext - ): Promise; - hookReduceArg0>( - hook: H, - args: any[], - reduce: Reduce, - hookContext?: HookContext - ): EnsurePromise; - hookReduceArg0Sync>( - hook: H, - args: any[], - reduce: Reduce, - hookContext?: HookContext - ): R; - hookReduceValue( - hook: string, - value: T | Promise, - args: any[], - reduce: Reduce, - hookContext?: HookContext - ): Promise; - hookReduceValueSync( - hook: string, - value: T, - args: any[], - reduce: Reduce, - hookContext?: HookContext - ): T; - hookSeq( - hook: H, - args: Args, - context?: HookContext - ): Promise; - hookSeqSync( - hook: H, - args: Args, - context?: HookContext - ): void; - startOutput(outputBundle: OutputBundleWithPlaceholders, assetFileNames: string): void; -} - -export type Reduce = (reduction: T, result: R, plugin: Plugin) => T; -export type HookContext = (context: PluginContext, plugin: Plugin) => PluginContext; - -export const ANONYMOUS_PLUGIN_PREFIX = 'at position '; - -const deprecatedHooks: { active: boolean; deprecated: string; replacement: string }[] = [ - { active: true, deprecated: 'ongenerate', replacement: 'generateBundle' }, - { active: true, deprecated: 'onwrite', replacement: 'generateBundle/writeBundle' }, - { active: true, deprecated: 'transformBundle', replacement: 'renderChunk' }, - { active: true, deprecated: 'transformChunk', replacement: 'renderChunk' }, - { active: false, deprecated: 'resolveAssetUrl', replacement: 'resolveFileUrl' } -]; - -function warnDeprecatedHooks(plugins: Plugin[], graph: Graph) { - for (const { active, deprecated, replacement } of deprecatedHooks) { - for (const plugin of plugins) { - if (deprecated in plugin) { - graph.warnDeprecation( - { - message: `The "${deprecated}" hook used by plugin ${plugin.name} is deprecated. The "${replacement}" hook should be used instead.`, - plugin: plugin.name - }, - active - ); - } - } - } -} - -export function throwPluginError( - err: string | RollupError, - plugin: string, - { hook, id }: { hook?: string; id?: string } = {} -): never { - if (typeof err === 'string') err = { message: err }; - if (err.code && err.code !== Errors.PLUGIN_ERROR) { - err.pluginCode = err.code; - } - err.code = Errors.PLUGIN_ERROR; - err.plugin = plugin; - if (hook) { - err.hook = hook; - } - if (id) { - err.id = id; - } - return error(err); -} - -export function createPluginDriver( - graph: Graph, - options: InputOptions, - pluginCache: Record | void, - watcher?: RollupWatcher -): PluginDriver { - warnDeprecatedHooks(options.plugins as Plugin[], graph); - - function getDeprecatedHookHandler( - handler: H, - handlerName: string, - newHandlerName: string, - pluginName: string, - acitveDeprecation: boolean - ): H { - let deprecationWarningShown = false; - return (((...args: any[]) => { - if (!deprecationWarningShown) { - deprecationWarningShown = true; - graph.warnDeprecation( - { - message: `The "this.${handlerName}" plugin context function used by plugin ${pluginName} is deprecated. The "this.${newHandlerName}" plugin context function should be used instead.`, - plugin: pluginName - }, - acitveDeprecation - ); - } - return handler(...args); - }) as unknown) as H; - } - - const plugins = [ - ...(options.plugins as Plugin[]), - getRollupDefaultPlugin(options.preserveSymlinks as boolean) - ]; - const fileEmitter = new FileEmitter(graph); - const existingPluginKeys = new Set(); - - const pluginContexts: PluginContext[] = plugins.map((plugin, pidx) => { - let cacheable = true; - if (typeof plugin.cacheKey !== 'string') { - if (plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) || existingPluginKeys.has(plugin.name)) { - cacheable = false; - } else { - existingPluginKeys.add(plugin.name); - } - } - - let cacheInstance: PluginCache; - if (!pluginCache) { - cacheInstance = noCache; - } else if (cacheable) { - const cacheKey = plugin.cacheKey || plugin.name; - cacheInstance = createPluginCache( - pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null)) - ); - } else { - cacheInstance = uncacheablePlugin(plugin.name); - } - - const context: PluginContext = { - addWatchFile(id) { - if (graph.phase >= BuildPhase.GENERATE) this.error(errInvalidRollupPhaseForAddWatchFile()); - graph.watchFiles[id] = true; - }, - cache: cacheInstance, - emitAsset: getDeprecatedHookHandler( - (name: string, source?: string | Buffer) => - fileEmitter.emitFile({ type: 'asset', name, source }), - 'emitAsset', - 'emitFile', - plugin.name, - false - ), - emitChunk: getDeprecatedHookHandler( - (id: string, options?: { name?: string }) => - fileEmitter.emitFile({ type: 'chunk', id, name: options && options.name }), - 'emitChunk', - 'emitFile', - plugin.name, - false - ), - emitFile: fileEmitter.emitFile, - error(err): never { - return throwPluginError(err, plugin.name); - }, - getAssetFileName: getDeprecatedHookHandler( - fileEmitter.getFileName, - 'getAssetFileName', - 'getFileName', - plugin.name, - false - ), - getChunkFileName: getDeprecatedHookHandler( - fileEmitter.getFileName, - 'getChunkFileName', - 'getFileName', - plugin.name, - false - ), - getFileName: fileEmitter.getFileName, - getModuleInfo(moduleId) { - const foundModule = graph.moduleById.get(moduleId); - if (foundModule == null) { - throw new Error(`Unable to find module ${moduleId}`); - } - - return { - hasModuleSideEffects: foundModule.moduleSideEffects, - id: foundModule.id, - importedIds: - foundModule instanceof ExternalModule - ? [] - : Array.from(foundModule.sources).map(id => foundModule.resolvedIds[id].id), - isEntry: foundModule instanceof Module && foundModule.isEntryPoint, - isExternal: foundModule instanceof ExternalModule - }; - }, - isExternal: getDeprecatedHookHandler( - (id: string, parentId: string, isResolved = false) => - graph.moduleLoader.isExternal(id, parentId, isResolved), - 'isExternal', - 'resolve', - plugin.name, - false - ), - meta: { - rollupVersion - }, - get moduleIds() { - return graph.moduleById.keys(); - }, - parse: graph.contextParse, - resolve(source, importer, options?: { skipSelf: boolean }) { - return graph.moduleLoader.resolveId( - source, - importer, - options && options.skipSelf ? pidx : null - ); - }, - resolveId: getDeprecatedHookHandler( - (source: string, importer: string) => - graph.moduleLoader - .resolveId(source, importer) - .then(resolveId => resolveId && resolveId.id), - 'resolveId', - 'resolve', - plugin.name, - false - ), - setAssetSource: fileEmitter.setAssetSource, - warn(warning) { - if (typeof warning === 'string') warning = { message: warning } as RollupWarning; - if (warning.code) warning.pluginCode = warning.code; - warning.code = 'PLUGIN_WARNING'; - warning.plugin = plugin.name; - graph.warn(warning); - }, - watcher: watcher - ? (() => { - let deprecationWarningShown = false; - - function deprecatedWatchListener(event: string, handler: () => void): EventEmitter { - if (!deprecationWarningShown) { - context.warn({ - code: 'PLUGIN_WATCHER_DEPRECATED', - message: `this.watcher usage is deprecated in plugins. Use the watchChange plugin hook and this.addWatchFile() instead.` - }); - deprecationWarningShown = true; - } - return (watcher as RollupWatcher).on(event, handler); - } - - return { - ...(watcher as EventEmitter), - addListener: deprecatedWatchListener, - on: deprecatedWatchListener - }; - })() - : (undefined as any) - }; - return context; - }); - - function runHookSync( - hookName: string, - args: any[], - pluginIndex: number, - permitValues = false, - hookContext?: HookContext - ): T { - const plugin = plugins[pluginIndex]; - let context = pluginContexts[pluginIndex]; - const hook = (plugin as any)[hookName]; - if (!hook) return undefined as any; - - if (hookContext) { - context = hookContext(context, plugin); - if (!context || context === pluginContexts[pluginIndex]) - throw new Error('Internal Rollup error: hookContext must return a new context object.'); - } - try { - // permit values allows values to be returned instead of a functional hook - if (typeof hook !== 'function') { - if (permitValues) return hook; - error({ - code: 'INVALID_PLUGIN_HOOK', - message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.` - }); - } - return hook.apply(context, args); - } catch (err) { - return throwPluginError(err, plugin.name, { hook: hookName }); - } - } - - function runHook( - hookName: string, - args: any[], - pluginIndex: number, - permitValues = false, - hookContext?: HookContext | null - ): Promise { - const plugin = plugins[pluginIndex]; - let context = pluginContexts[pluginIndex]; - const hook = (plugin as any)[hookName]; - if (!hook) return undefined as any; - - if (hookContext) { - context = hookContext(context, plugin); - if (!context || context === pluginContexts[pluginIndex]) - throw new Error('Internal Rollup error: hookContext must return a new context object.'); - } - return Promise.resolve() - .then(() => { - // permit values allows values to be returned instead of a functional hook - if (typeof hook !== 'function') { - if (permitValues) return hook; - error({ - code: 'INVALID_PLUGIN_HOOK', - message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.` - }); - } - return hook.apply(context, args); - }) - .catch(err => throwPluginError(err, plugin.name, { hook: hookName })); - } - - const pluginDriver: PluginDriver = { - emitFile: fileEmitter.emitFile, - finaliseAssets() { - fileEmitter.assertAssetsFinalized(); - }, - getFileName: fileEmitter.getFileName, - - // chains, ignores returns - hookSeq(name, args, hookContext) { - let promise: Promise = Promise.resolve() as any; - for (let i = 0; i < plugins.length; i++) - promise = promise.then(() => runHook(name, args as any[], i, false, hookContext)); - return promise; - }, - - // chains, ignores returns - hookSeqSync(name, args, hookContext) { - for (let i = 0; i < plugins.length; i++) - runHookSync(name, args as any[], i, false, hookContext); - }, - - // chains, first non-null result stops and returns - hookFirst(name, args, hookContext, skip) { - let promise: Promise = Promise.resolve(); - for (let i = 0; i < plugins.length; i++) { - if (skip === i) continue; - promise = promise.then((result: any) => { - if (result != null) return result; - return runHook(name, args as any[], i, false, hookContext); - }); - } - return promise; - }, - - // chains synchronously, first non-null result stops and returns - hookFirstSync(name, args?, hookContext?) { - for (let i = 0; i < plugins.length; i++) { - const result = runHookSync(name, args, i, false, hookContext); - if (result != null) return result as any; - } - return null; - }, - - // parallel, ignores returns - hookParallel(name, args, hookContext) { - const promises: Promise[] = []; - for (let i = 0; i < plugins.length; i++) { - const hookPromise = runHook(name, args as any[], i, false, hookContext); - if (!hookPromise) continue; - promises.push(hookPromise); - } - return Promise.all(promises).then(() => {}); - }, - - // chains, reduces returns of type R, to type T, handling the reduced value as the first hook argument - hookReduceArg0(name, [arg0, ...args], reduce, hookContext) { - let promise = Promise.resolve(arg0); - for (let i = 0; i < plugins.length; i++) { - promise = promise.then(arg0 => { - const hookPromise = runHook(name, [arg0, ...args], i, false, hookContext); - if (!hookPromise) return arg0; - return hookPromise.then((result: any) => - reduce.call(pluginContexts[i], arg0, result, plugins[i]) - ); - }); - } - return promise; - }, - - // chains synchronously, reduces returns of type R, to type T, handling the reduced value as the first hook argument - hookReduceArg0Sync(name, [arg0, ...args], reduce, hookContext) { - for (let i = 0; i < plugins.length; i++) { - const result: any = runHookSync(name, [arg0, ...args], i, false, hookContext); - arg0 = reduce.call(pluginContexts[i], arg0, result, plugins[i]); - } - return arg0; - }, - - // chains, reduces returns of type R, to type T, handling the reduced value separately. permits hooks as values. - hookReduceValue(name, initial, args, reduce, hookContext) { - let promise = Promise.resolve(initial); - for (let i = 0; i < plugins.length; i++) { - promise = promise.then(value => { - const hookPromise = runHook(name, args, i, true, hookContext); - if (!hookPromise) return value; - return hookPromise.then((result: any) => - reduce.call(pluginContexts[i], value, result, plugins[i]) - ); - }); - } - return promise; - }, - - // chains, reduces returns of type R, to type T, handling the reduced value separately. permits hooks as values. - hookReduceValueSync(name, initial, args, reduce, hookContext) { - let acc = initial; - for (let i = 0; i < plugins.length; i++) { - const result: any = runHookSync(name, args, i, true, hookContext); - acc = reduce.call(pluginContexts[i], acc, result, plugins[i]); - } - return acc; - }, - - startOutput(outputBundle: OutputBundleWithPlaceholders, assetFileNames: string): void { - fileEmitter.startOutput(outputBundle, assetFileNames); - } - }; - - return pluginDriver; -} - -export function createPluginCache(cache: SerializablePluginCache): PluginCache { - return { - has(id: string) { - const item = cache[id]; - if (!item) return false; - item[0] = 0; - return true; - }, - get(id: string) { - const item = cache[id]; - if (!item) return undefined; - item[0] = 0; - return item[1]; - }, - set(id: string, value: any) { - cache[id] = [0, value]; - }, - delete(id: string) { - return delete cache[id]; - } - }; -} - -export function trackPluginCache(pluginCache: PluginCache) { - const result = { used: false, cache: (undefined as any) as PluginCache }; - result.cache = { - has(id: string) { - result.used = true; - return pluginCache.has(id); - }, - get(id: string) { - result.used = true; - return pluginCache.get(id); - }, - set(id: string, value: any) { - result.used = true; - return pluginCache.set(id, value); - }, - delete(id: string) { - result.used = true; - return pluginCache.delete(id); - } - }; - return result; -} - -const noCache: PluginCache = { - has() { - return false; - }, - get() { - return undefined as any; - }, - set() {}, - delete() { - return false; - } -}; - -function uncacheablePluginError(pluginName: string) { - if (pluginName.startsWith(ANONYMOUS_PLUGIN_PREFIX)) - error({ - code: 'ANONYMOUS_PLUGIN_CACHE', - message: - 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.' - }); - else - error({ - code: 'DUPLICATE_PLUGIN_NAME', - message: `The plugin name ${pluginName} is being used twice in the same build. Plugin names must be distinct or provide a cacheKey (please post an issue to the plugin if you are a plugin user).` - }); -} - -const uncacheablePlugin: (pluginName: string) => PluginCache = pluginName => ({ - has() { - uncacheablePluginError(pluginName); - return false; - }, - get() { - uncacheablePluginError(pluginName); - return undefined as any; - }, - set() { - uncacheablePluginError(pluginName); - }, - delete() { - uncacheablePluginError(pluginName); - return false; - } -}); diff --git a/src/utils/pluginUtils.ts b/src/utils/pluginUtils.ts new file mode 100644 index 00000000000..e855212bf4b --- /dev/null +++ b/src/utils/pluginUtils.ts @@ -0,0 +1,50 @@ +import Graph from '../Graph'; +import { Plugin, RollupError } from '../rollup/types'; +import { error, Errors } from './error'; + +export const ANONYMOUS_PLUGIN_PREFIX = 'at position '; +export const ANONYMOUS_OUTPUT_PLUGIN_PREFIX = 'at output position '; + +export function throwPluginError( + err: string | RollupError, + plugin: string, + { hook, id }: { hook?: string; id?: string } = {} +): never { + if (typeof err === 'string') err = { message: err }; + if (err.code && err.code !== Errors.PLUGIN_ERROR) { + err.pluginCode = err.code; + } + err.code = Errors.PLUGIN_ERROR; + err.plugin = plugin; + if (hook) { + err.hook = hook; + } + if (id) { + err.id = id; + } + return error(err); +} + +export const deprecatedHooks: { active: boolean; deprecated: string; replacement: string }[] = [ + { active: true, deprecated: 'ongenerate', replacement: 'generateBundle' }, + { active: true, deprecated: 'onwrite', replacement: 'generateBundle/writeBundle' }, + { active: true, deprecated: 'transformBundle', replacement: 'renderChunk' }, + { active: true, deprecated: 'transformChunk', replacement: 'renderChunk' }, + { active: false, deprecated: 'resolveAssetUrl', replacement: 'resolveFileUrl' } +]; + +export function warnDeprecatedHooks(plugins: Plugin[], graph: Graph) { + for (const { active, deprecated, replacement } of deprecatedHooks) { + for (const plugin of plugins) { + if (deprecated in plugin) { + graph.warnDeprecation( + { + message: `The "${deprecated}" hook used by plugin ${plugin.name} is deprecated. The "${replacement}" hook should be used instead.`, + plugin: plugin.name + }, + active + ); + } + } + } +} diff --git a/src/utils/renderChunk.ts b/src/utils/renderChunk.ts index d53be7c376b..64e58abf9f5 100644 --- a/src/utils/renderChunk.ts +++ b/src/utils/renderChunk.ts @@ -1,5 +1,4 @@ import Chunk from '../Chunk'; -import Graph from '../Graph'; import { DecodedSourceMapOrMissing, OutputOptions, @@ -9,19 +8,20 @@ import { } from '../rollup/types'; import { decodedSourcemap } from './decodedSourcemap'; import { error } from './error'; +import { PluginDriver } from './PluginDriver'; export default function renderChunk({ - graph, chunk, - renderChunk, code, - sourcemapChain, - options + options, + outputPluginDriver, + renderChunk, + sourcemapChain }: { chunk: Chunk; code: string; - graph: Graph; options: OutputOptions; + outputPluginDriver: PluginDriver; renderChunk: RenderedChunk; sourcemapChain: DecodedSourceMapOrMissing[]; }): Promise { @@ -49,11 +49,11 @@ export default function renderChunk({ let inTransformBundle = false; let inRenderChunk = true; - return graph.pluginDriver + return outputPluginDriver .hookReduceArg0('renderChunk', [code, renderChunk, options], renderChunkReducer) .then(code => { inRenderChunk = false; - return graph.pluginDriver.hookReduceArg0( + return outputPluginDriver.hookReduceArg0( 'transformChunk', [code, options, chunk], renderChunkReducer @@ -61,7 +61,7 @@ export default function renderChunk({ }) .then(code => { inTransformBundle = true; - return graph.pluginDriver.hookReduceArg0( + return outputPluginDriver.hookReduceArg0( 'transformBundle', [code, options, chunk], renderChunkReducer diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 2e6d6f382bb..3e16df29471 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -17,7 +17,8 @@ import { collapseSourcemap } from './collapseSourcemaps'; import { decodedSourcemap } from './decodedSourcemap'; import { augmentCodeLocation } from './error'; import { dirname, resolve } from './path'; -import { throwPluginError, trackPluginCache } from './pluginDriver'; +import { getTrackedPluginCache } from './PluginCache'; +import { throwPluginError } from './pluginUtils'; export default function transform( graph: Graph, @@ -106,7 +107,7 @@ export default function transform( (pluginContext, plugin) => { curPlugin = plugin; if (curPlugin.cacheKey) customTransformCache = true; - else trackedPluginCache = trackPluginCache(pluginContext.cache); + else trackedPluginCache = getTrackedPluginCache(pluginContext.cache); return { ...pluginContext, cache: trackedPluginCache ? trackedPluginCache.cache : pluginContext.cache, diff --git a/test/chunking-form/samples/emit-same-file/_config.js b/test/chunking-form/samples/emit-same-file/_config.js deleted file mode 100644 index bad65a0f0d7..00000000000 --- a/test/chunking-form/samples/emit-same-file/_config.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - description: - 'does not throw an error if multiple files with the same name are emitted (until #3174 is fixed)', - options: { - input: 'main.js', - plugins: [ - { - generateBundle() { - this.emitFile({ type: 'asset', fileName: 'myfile', source: 'abc' }); - this.emitFile({ type: 'asset', fileName: 'myfile', source: 'abc' }); - } - } - ] - } -}; diff --git a/test/cli/samples/multiple-targets-different-plugins/_config.js b/test/cli/samples/multiple-targets-different-plugins/_config.js new file mode 100644 index 00000000000..e9ded7da1c6 --- /dev/null +++ b/test/cli/samples/multiple-targets-different-plugins/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'generates multiple output files, only one of which is minified', + command: 'rollup -c' +}; diff --git a/test/cli/samples/multiple-targets-different-plugins/_expected/main.js b/test/cli/samples/multiple-targets-different-plugins/_expected/main.js new file mode 100644 index 00000000000..7fb07e7ab0f --- /dev/null +++ b/test/cli/samples/multiple-targets-different-plugins/_expected/main.js @@ -0,0 +1,7 @@ +'use strict'; + +const Hello = 1; +console.log(Hello); +var main = 0; + +module.exports = main; diff --git a/test/cli/samples/multiple-targets-different-plugins/_expected/minified.js b/test/cli/samples/multiple-targets-different-plugins/_expected/minified.js new file mode 100644 index 00000000000..d5fc32bfdf8 --- /dev/null +++ b/test/cli/samples/multiple-targets-different-plugins/_expected/minified.js @@ -0,0 +1 @@ +"use strict";const Hello=1;console.log(1);var main=0;module.exports=main; diff --git a/test/cli/samples/multiple-targets-different-plugins/main.js b/test/cli/samples/multiple-targets-different-plugins/main.js new file mode 100644 index 00000000000..d9c8e76411d --- /dev/null +++ b/test/cli/samples/multiple-targets-different-plugins/main.js @@ -0,0 +1,3 @@ +const Hello = 1; +console.log(Hello); +export default 0; diff --git a/test/cli/samples/multiple-targets-different-plugins/rollup.config.js b/test/cli/samples/multiple-targets-different-plugins/rollup.config.js new file mode 100644 index 00000000000..a0d9108c282 --- /dev/null +++ b/test/cli/samples/multiple-targets-different-plugins/rollup.config.js @@ -0,0 +1,16 @@ +import { terser } from 'rollup-plugin-terser'; + +export default { + input: 'main.js', + output: [ + { + format: 'cjs', + file: '_actual/main.js' + }, + { + format: 'cjs', + file: '_actual/minified.js', + plugins: [terser()] + } + ] +}; diff --git a/test/form/samples/per-output-plugins/_config.js b/test/form/samples/per-output-plugins/_config.js new file mode 100644 index 00000000000..8045837e438 --- /dev/null +++ b/test/form/samples/per-output-plugins/_config.js @@ -0,0 +1,20 @@ +module.exports = { + description: 'allows specifying per-output plugins', + options: { + output: { + plugins: [ + { + name: 'test-plugin', + renderChunk(code, chunkDescription, { format }) { + return code.replace(42, `'${format}'`); + } + }, + { + renderChunk(code, chunkDescription, { format }) { + return code.replace(43, `'!${format}!'`); + } + } + ] + } + } +}; diff --git a/test/form/samples/per-output-plugins/_expected/amd.js b/test/form/samples/per-output-plugins/_expected/amd.js new file mode 100644 index 00000000000..a12305aff0d --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/amd.js @@ -0,0 +1,5 @@ +define(function () { 'use strict'; + + console.log('amd', '!amd!'); + +}); diff --git a/test/form/samples/per-output-plugins/_expected/cjs.js b/test/form/samples/per-output-plugins/_expected/cjs.js new file mode 100644 index 00000000000..9b162a6eb87 --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/cjs.js @@ -0,0 +1,3 @@ +'use strict'; + +console.log('cjs', '!cjs!'); diff --git a/test/form/samples/per-output-plugins/_expected/es.js b/test/form/samples/per-output-plugins/_expected/es.js new file mode 100644 index 00000000000..dd41435976a --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/es.js @@ -0,0 +1 @@ +console.log('es', '!es!'); diff --git a/test/form/samples/per-output-plugins/_expected/iife.js b/test/form/samples/per-output-plugins/_expected/iife.js new file mode 100644 index 00000000000..42c0f688f00 --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/iife.js @@ -0,0 +1,6 @@ +(function () { + 'use strict'; + + console.log('iife', '!iife!'); + +}()); diff --git a/test/form/samples/per-output-plugins/_expected/system.js b/test/form/samples/per-output-plugins/_expected/system.js new file mode 100644 index 00000000000..600f1bdfbd1 --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/system.js @@ -0,0 +1,10 @@ +System.register([], function () { + 'use strict'; + return { + execute: function () { + + console.log('system', '!system!'); + + } + }; +}); diff --git a/test/form/samples/per-output-plugins/_expected/umd.js b/test/form/samples/per-output-plugins/_expected/umd.js new file mode 100644 index 00000000000..0fddc9f20f8 --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/umd.js @@ -0,0 +1,8 @@ +(function (factory) { + typeof define === 'function' && define.amd ? define(factory) : + factory(); +}((function () { 'use strict'; + + console.log('umd', '!umd!'); + +}))); diff --git a/test/form/samples/per-output-plugins/main.js b/test/form/samples/per-output-plugins/main.js new file mode 100644 index 00000000000..fdd64120dc6 --- /dev/null +++ b/test/form/samples/per-output-plugins/main.js @@ -0,0 +1 @@ +console.log(42, 43); diff --git a/test/function/samples/emit-file/emit-same-file/_config.js b/test/function/samples/emit-file/emit-same-file/_config.js new file mode 100644 index 00000000000..a0266d5791b --- /dev/null +++ b/test/function/samples/emit-file/emit-same-file/_config.js @@ -0,0 +1,30 @@ +module.exports = { + description: 'warns if multiple files with the same name are emitted', + options: { + input: 'main.js', + plugins: [ + { + buildStart() { + this.emitFile({ type: 'asset', fileName: 'buildStart', source: 'abc' }); + }, + generateBundle() { + this.emitFile({ type: 'asset', fileName: 'buildStart', source: 'abc' }); + this.emitFile({ type: 'asset', fileName: 'generateBundle', source: 'abc' }); + this.emitFile({ type: 'asset', fileName: 'generateBundle', source: 'abc' }); + } + } + ] + }, + warnings: [ + { + code: 'FILE_NAME_CONFLICT', + message: + 'The emitted file "buildStart" overwrites a previously emitted file of the same name.' + }, + { + code: 'FILE_NAME_CONFLICT', + message: + 'The emitted file "generateBundle" overwrites a previously emitted file of the same name.' + } + ] +}; diff --git a/test/chunking-form/samples/emit-same-file/_expected/amd/main.js b/test/function/samples/emit-file/emit-same-file/_expected/amd/main.js similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/amd/main.js rename to test/function/samples/emit-file/emit-same-file/_expected/amd/main.js diff --git a/test/chunking-form/samples/emit-same-file/_expected/amd/myfile b/test/function/samples/emit-file/emit-same-file/_expected/amd/myfile similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/amd/myfile rename to test/function/samples/emit-file/emit-same-file/_expected/amd/myfile diff --git a/test/chunking-form/samples/emit-same-file/_expected/cjs/main.js b/test/function/samples/emit-file/emit-same-file/_expected/cjs/main.js similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/cjs/main.js rename to test/function/samples/emit-file/emit-same-file/_expected/cjs/main.js diff --git a/test/chunking-form/samples/emit-same-file/_expected/cjs/myfile b/test/function/samples/emit-file/emit-same-file/_expected/cjs/myfile similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/cjs/myfile rename to test/function/samples/emit-file/emit-same-file/_expected/cjs/myfile diff --git a/test/chunking-form/samples/emit-same-file/_expected/es/main.js b/test/function/samples/emit-file/emit-same-file/_expected/es/main.js similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/es/main.js rename to test/function/samples/emit-file/emit-same-file/_expected/es/main.js diff --git a/test/chunking-form/samples/emit-same-file/_expected/es/myfile b/test/function/samples/emit-file/emit-same-file/_expected/es/myfile similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/es/myfile rename to test/function/samples/emit-file/emit-same-file/_expected/es/myfile diff --git a/test/chunking-form/samples/emit-same-file/_expected/system/main.js b/test/function/samples/emit-file/emit-same-file/_expected/system/main.js similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/system/main.js rename to test/function/samples/emit-file/emit-same-file/_expected/system/main.js diff --git a/test/chunking-form/samples/emit-same-file/_expected/system/myfile b/test/function/samples/emit-file/emit-same-file/_expected/system/myfile similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/system/myfile rename to test/function/samples/emit-file/emit-same-file/_expected/system/myfile diff --git a/test/chunking-form/samples/emit-same-file/main.js b/test/function/samples/emit-file/emit-same-file/main.js similarity index 100% rename from test/chunking-form/samples/emit-same-file/main.js rename to test/function/samples/emit-file/emit-same-file/main.js diff --git a/test/function/samples/file-and-dir/_config.js b/test/function/samples/file-and-dir/_config.js new file mode 100644 index 00000000000..5a777946e19 --- /dev/null +++ b/test/function/samples/file-and-dir/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'throws when using both the file and the dir option', + options: { + output: { file: 'bundle.js', dir: 'dist' } + }, + generateError: { + code: 'INVALID_OPTION', + message: + 'You must set either "output.file" for a single-file build or "output.dir" when generating multiple chunks.' + } +}; diff --git a/test/function/samples/file-and-dir/foo.js b/test/function/samples/file-and-dir/foo.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/file-and-dir/foo.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/function/samples/file-and-dir/main.js b/test/function/samples/file-and-dir/main.js new file mode 100644 index 00000000000..a25cfbd9058 --- /dev/null +++ b/test/function/samples/file-and-dir/main.js @@ -0,0 +1 @@ +export default () => import('./foo.js'); diff --git a/test/function/samples/iife-code-splitting/_config.js b/test/function/samples/iife-code-splitting/_config.js new file mode 100644 index 00000000000..ae4602851f7 --- /dev/null +++ b/test/function/samples/iife-code-splitting/_config.js @@ -0,0 +1,10 @@ +module.exports = { + description: 'throws when generating multiple chunks for an IIFE build', + options: { + output: { format: 'iife' } + }, + generateError: { + code: 'INVALID_OPTION', + message: 'UMD and IIFE output formats are not supported for code-splitting builds.' + } +}; diff --git a/test/function/samples/iife-code-splitting/foo.js b/test/function/samples/iife-code-splitting/foo.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/iife-code-splitting/foo.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/function/samples/iife-code-splitting/main.js b/test/function/samples/iife-code-splitting/main.js new file mode 100644 index 00000000000..a25cfbd9058 --- /dev/null +++ b/test/function/samples/iife-code-splitting/main.js @@ -0,0 +1 @@ +export default () => import('./foo.js'); diff --git a/test/function/samples/invalid-top-level-await/_config.js b/test/function/samples/invalid-top-level-await/_config.js new file mode 100644 index 00000000000..d0ffc48dbd9 --- /dev/null +++ b/test/function/samples/invalid-top-level-await/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'throws for invalid top-level-await format', + options: { + experimentalTopLevelAwait: true + }, + generateError: { + code: 'INVALID_TLA_FORMAT', + message: + 'Module format cjs does not support top-level await. Use the "es" or "system" output formats rather.' + } +}; diff --git a/test/function/samples/invalid-top-level-await/main.js b/test/function/samples/invalid-top-level-await/main.js new file mode 100644 index 00000000000..fb59627860e --- /dev/null +++ b/test/function/samples/invalid-top-level-await/main.js @@ -0,0 +1 @@ +await Promise.resolve(); diff --git a/test/function/samples/non-function-hook-async/_config.js b/test/function/samples/non-function-hook-async/_config.js new file mode 100644 index 00000000000..d17469d3926 --- /dev/null +++ b/test/function/samples/non-function-hook-async/_config.js @@ -0,0 +1,15 @@ +module.exports = { + description: 'throws when providing a value for an async function hook', + options: { + plugins: { + resolveId: 'value' + } + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'resolveId', + message: 'Error running plugin hook resolveId for at position 1, expected a function hook.', + plugin: 'at position 1', + pluginCode: 'INVALID_PLUGIN_HOOK' + } +}; diff --git a/test/function/samples/non-function-hook-async/foo.js b/test/function/samples/non-function-hook-async/foo.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/non-function-hook-async/foo.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/function/samples/non-function-hook-async/main.js b/test/function/samples/non-function-hook-async/main.js new file mode 100644 index 00000000000..a25cfbd9058 --- /dev/null +++ b/test/function/samples/non-function-hook-async/main.js @@ -0,0 +1 @@ +export default () => import('./foo.js'); diff --git a/test/function/samples/non-function-hook-sync/_config.js b/test/function/samples/non-function-hook-sync/_config.js new file mode 100644 index 00000000000..c447f84ec77 --- /dev/null +++ b/test/function/samples/non-function-hook-sync/_config.js @@ -0,0 +1,15 @@ +module.exports = { + description: 'throws when providing a value for a sync function hook', + options: { + plugins: { + outputOptions: 'value' + } + }, + generateError: { + code: 'PLUGIN_ERROR', + hook: 'outputOptions', + message: 'Error running plugin hook outputOptions for at position 1, expected a function hook.', + plugin: 'at position 1', + pluginCode: 'INVALID_PLUGIN_HOOK' + } +}; diff --git a/test/function/samples/non-function-hook-sync/foo.js b/test/function/samples/non-function-hook-sync/foo.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/non-function-hook-sync/foo.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/function/samples/non-function-hook-sync/main.js b/test/function/samples/non-function-hook-sync/main.js new file mode 100644 index 00000000000..a25cfbd9058 --- /dev/null +++ b/test/function/samples/non-function-hook-sync/main.js @@ -0,0 +1 @@ +export default () => import('./foo.js'); diff --git a/test/function/samples/options-in-renderstart/_config.js b/test/function/samples/options-in-renderstart/_config.js new file mode 100644 index 00000000000..3129a8b36ea --- /dev/null +++ b/test/function/samples/options-in-renderstart/_config.js @@ -0,0 +1,33 @@ +const assert = require('assert'); +const checkedOptions = []; + +module.exports = { + description: 'makes input and output options available in renderStart', + options: { + context: 'global', + plugins: { + name: 'input-plugin', + renderStart(outputOptions, inputOptions) { + checkedOptions.push('input-plugin', outputOptions.format, inputOptions.context); + } + }, + output: { + plugins: { + name: 'output-plugin', + renderStart(outputOptions, inputOptions) { + checkedOptions.push('output-plugin', outputOptions.format, inputOptions.context); + } + } + } + }, + exports: () => { + assert.deepStrictEqual(checkedOptions, [ + 'output-plugin', + 'cjs', + 'global', + 'input-plugin', + 'cjs', + 'global' + ]); + } +}; diff --git a/test/function/samples/options-in-renderstart/main.js b/test/function/samples/options-in-renderstart/main.js new file mode 100644 index 00000000000..5dff5e2ec72 --- /dev/null +++ b/test/function/samples/options-in-renderstart/main.js @@ -0,0 +1 @@ +assert.ok(this); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_config.js b/test/function/samples/per-output-plugins-warn-hooks/_config.js new file mode 100644 index 00000000000..3583c674719 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_config.js @@ -0,0 +1,72 @@ +module.exports = { + description: 'warns when input hooks are used in output plugins', + options: { + output: { + plugins: [ + { + name: 'test-plugin', + options() {}, + buildStart() {}, + resolveId() {}, + load() {}, + transform() {}, + buildEnd() {}, + outputOptions() {}, + renderStart() {}, + banner() {}, + footer() {}, + intro() {}, + outro() {}, + resolveDynamicImport() {}, + resolveFileUrl() {}, + resolveImportMeta() {}, + augmentChunkHash() {}, + renderChunk() {}, + generateBundle() {}, + writeBundle() {}, + renderError() {} + }, + { + buildStart() {} + } + ] + } + }, + warnings: [ + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "options" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "buildStart" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "resolveId" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "load" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "transform" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "buildEnd" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "buildStart" hook used by the output plugin at output position 2 is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + } + ] +}; diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/amd.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/amd.js new file mode 100644 index 00000000000..1d43a8d4670 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/amd.js @@ -0,0 +1,5 @@ +define(function () { 'use strict'; + + console.log('amd'); + +}); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/cjs.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/cjs.js new file mode 100644 index 00000000000..ff37dabca27 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/cjs.js @@ -0,0 +1,3 @@ +'use strict'; + +console.log('cjs'); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/es.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/es.js new file mode 100644 index 00000000000..cbe9810363d --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/es.js @@ -0,0 +1 @@ +console.log('es'); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/iife.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/iife.js new file mode 100644 index 00000000000..b64ab69e189 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/iife.js @@ -0,0 +1,6 @@ +(function () { + 'use strict'; + + console.log('iife'); + +}()); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/system.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/system.js new file mode 100644 index 00000000000..9cd89c4c3a0 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/system.js @@ -0,0 +1,10 @@ +System.register([], function () { + 'use strict'; + return { + execute: function () { + + console.log('system'); + + } + }; +}); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/umd.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/umd.js new file mode 100644 index 00000000000..cece26c2f48 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/umd.js @@ -0,0 +1,8 @@ +(function (factory) { + typeof define === 'function' && define.amd ? define(factory) : + factory(); +}((function () { 'use strict'; + + console.log('umd'); + +}))); diff --git a/test/function/samples/per-output-plugins-warn-hooks/main.js b/test/function/samples/per-output-plugins-warn-hooks/main.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/main.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/function/samples/plugin-cache/anonymous-delete/_config.js b/test/function/samples/plugin-cache/anonymous-delete/_config.js new file mode 100644 index 00000000000..8d9a9ef6e1a --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-delete/_config.js @@ -0,0 +1,18 @@ +module.exports = { + description: 'throws for anonymous plugins deleting from the cache', + options: { + plugins: { + buildStart() { + this.cache.delete('asdf'); + } + } + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'buildStart', + message: + 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.', + plugin: 'at position 1', + pluginCode: 'ANONYMOUS_PLUGIN_CACHE' + } +}; diff --git a/test/function/samples/plugin-cache/anonymous-delete/main.js b/test/function/samples/plugin-cache/anonymous-delete/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-delete/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/function/samples/plugin-cache/anonymous-get/_config.js b/test/function/samples/plugin-cache/anonymous-get/_config.js new file mode 100644 index 00000000000..9a25655aa12 --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-get/_config.js @@ -0,0 +1,18 @@ +module.exports = { + description: 'throws for anonymous plugins reading the cache', + options: { + plugins: { + buildStart() { + this.cache.get('asdf'); + } + } + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'buildStart', + message: + 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.', + plugin: 'at position 1', + pluginCode: 'ANONYMOUS_PLUGIN_CACHE' + } +}; diff --git a/test/function/samples/plugin-cache/anonymous-get/main.js b/test/function/samples/plugin-cache/anonymous-get/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-get/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/function/samples/plugin-cache/anonymous-has/_config.js b/test/function/samples/plugin-cache/anonymous-has/_config.js new file mode 100644 index 00000000000..f6d37f0485e --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-has/_config.js @@ -0,0 +1,18 @@ +module.exports = { + description: 'throws for anonymous plugins checking the cache', + options: { + plugins: { + buildStart() { + this.cache.has('asdf'); + } + } + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'buildStart', + message: + 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.', + plugin: 'at position 1', + pluginCode: 'ANONYMOUS_PLUGIN_CACHE' + } +}; diff --git a/test/function/samples/plugin-cache/anonymous-has/main.js b/test/function/samples/plugin-cache/anonymous-has/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-has/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/function/samples/plugin-cache/anonymous-set/_config.js b/test/function/samples/plugin-cache/anonymous-set/_config.js new file mode 100644 index 00000000000..c12e533586c --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-set/_config.js @@ -0,0 +1,18 @@ +module.exports = { + description: 'throws for anonymous plugins adding to the cache', + options: { + plugins: { + buildStart() { + this.cache.set('asdf', 'asdf'); + } + } + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'buildStart', + message: + 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.', + plugin: 'at position 1', + pluginCode: 'ANONYMOUS_PLUGIN_CACHE' + } +}; diff --git a/test/function/samples/plugin-cache/anonymous-set/main.js b/test/function/samples/plugin-cache/anonymous-set/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-set/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/function/samples/plugin-cache/duplicate-names-no-cache/_config.js b/test/function/samples/plugin-cache/duplicate-names-no-cache/_config.js new file mode 100644 index 00000000000..5acf2e8b233 --- /dev/null +++ b/test/function/samples/plugin-cache/duplicate-names-no-cache/_config.js @@ -0,0 +1,15 @@ +module.exports = { + description: 'allows plugins to have the same name if they do not access the cache', + options: { + plugins: [ + { + name: 'test-plugin', + buildStart() {} + }, + { + name: 'test-plugin', + buildStart() {} + } + ] + } +}; diff --git a/test/function/samples/plugin-cache/duplicate-names-no-cache/main.js b/test/function/samples/plugin-cache/duplicate-names-no-cache/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/duplicate-names-no-cache/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/function/samples/plugin-cache/duplicate-names/_config.js b/test/function/samples/plugin-cache/duplicate-names/_config.js new file mode 100644 index 00000000000..bf9f5a7af39 --- /dev/null +++ b/test/function/samples/plugin-cache/duplicate-names/_config.js @@ -0,0 +1,27 @@ +module.exports = { + description: 'throws if two plugins with the same name and no cache key access the cache', + options: { + plugins: [ + { + name: 'test-plugin', + buildStart() { + this.cache.set('asdf', 'asdf'); + } + }, + { + name: 'test-plugin', + buildStart() { + this.cache.set('asdf', 'asdf'); + } + } + ] + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'buildStart', + message: + 'The plugin name test-plugin is being used twice in the same build. Plugin names must be distinct or provide a cacheKey (please post an issue to the plugin if you are a plugin user).', + plugin: 'test-plugin', + pluginCode: 'DUPLICATE_PLUGIN_NAME' + } +}; diff --git a/test/function/samples/plugin-cache/duplicate-names/main.js b/test/function/samples/plugin-cache/duplicate-names/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/duplicate-names/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/hooks/index.js b/test/hooks/index.js index 2e4d800ca83..c8e083a3b2a 100644 --- a/test/hooks/index.js +++ b/test/hooks/index.js @@ -58,7 +58,7 @@ describe('hooks', () => { }) ) .then(({ output }) => { - assert.equal(output[0].code, `new banner\n'use strict';\n\nalert('hello');\n`); + assert.strictEqual(output[0].code, `new banner\n'use strict';\n\nalert('hello');\n`); })); it('allows to replace file with dir in the outputOptions hook', () => @@ -118,8 +118,8 @@ describe('hooks', () => { ] }) .then(bundle => { - assert.equal(buildStartCnt, 1); - assert.equal(buildEndCnt, 1); + assert.strictEqual(buildStartCnt, 1); + assert.strictEqual(buildEndCnt, 1); return rollup.rollup({ input: 'input', @@ -140,8 +140,8 @@ describe('hooks', () => { assert.ok(err); }) .then(() => { - assert.equal(buildStartCnt, 2); - assert.equal(buildEndCnt, 2); + assert.strictEqual(buildStartCnt, 2); + assert.strictEqual(buildEndCnt, 2); }); }); @@ -152,10 +152,10 @@ describe('hooks', () => { input: 'input', onwarn(warning) { if (callCnt === 0) { - assert.equal(warning.message, 'build start'); + assert.strictEqual(warning.message, 'build start'); callCnt++; } else if (callCnt === 1) { - assert.equal(warning.message, 'build end'); + assert.strictEqual(warning.message, 'build end'); callCnt++; } }, @@ -172,7 +172,7 @@ describe('hooks', () => { ] }) .then(() => { - assert.equal(callCnt, 2); + assert.strictEqual(callCnt, 2); }); }); @@ -188,7 +188,7 @@ describe('hooks', () => { this.error('build start error'); }, buildEnd(error) { - assert.equal(error.message, 'build start error'); + assert.strictEqual(error.message, 'build start error'); handledError = true; } } @@ -196,7 +196,7 @@ describe('hooks', () => { }) .catch(error => { assert.ok(handledError); - assert.equal(error.message, 'build start error'); + assert.strictEqual(error.message, 'build start error'); }) .then(() => { assert.ok(handledError); @@ -211,8 +211,8 @@ describe('hooks', () => { loader({ input: `alert('hello')` }), { buildStart() { - assert.equal(this.isExternal('test'), true); - assert.equal(this.isExternal('another'), false); + assert.strictEqual(this.isExternal('test'), true); + assert.strictEqual(this.isExternal('another'), false); } } ] @@ -239,7 +239,7 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output: [output] }) => { - assert.equal(output.code, `alert('hello');\n`); + assert.strictEqual(output.code, `alert('hello');\n`); })); it('caches chunk emission in transform hook', () => { @@ -266,12 +266,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); return rollup.rollup({ cache, @@ -291,12 +291,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); return rollup.rollup({ cache, @@ -313,12 +313,41 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); + }); + }); + + it('does not overwrite files in other outputs when emitting assets during generate', () => { + return rollup + .rollup({ + input: 'input', + plugins: [ + loader({ input: 'export default 42;' }), + { + generateBundle(outputOptions) { + this.emitFile({ type: 'asset', source: outputOptions.format }); + } + } + ] + }) + .then(bundle => + Promise.all([ + bundle.generate({ format: 'es', assetFileNames: 'asset' }), + bundle.generate({ format: 'cjs', assetFileNames: 'asset' }) + ]) + ) + .then(([{ output: output1 }, { output: output2 }]) => { + assert.strictEqual(output1.length, 2, 'output1'); + assert.strictEqual(output1[1].fileName, 'asset'); + assert.strictEqual(output1[1].source, 'es'); + assert.strictEqual(output2.length, 2, 'output2'); + assert.strictEqual(output2[1].fileName, 'asset'); + assert.strictEqual(output2[1].source, 'cjs'); }); }); @@ -345,12 +374,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -370,12 +399,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -392,12 +421,12 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); }); }); @@ -439,14 +468,14 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `console.log('imported');\n\n` + `var input = new URL('assets/test-09aeb845.ext', import.meta.url).href;\n\n` + `export default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-09aeb845.ext'); - assert.equal(output[1].source, 'first run'); + assert.strictEqual(output[1].fileName, 'assets/test-09aeb845.ext'); + assert.strictEqual(output[1].source, 'first run'); return rollup.rollup({ cache, @@ -472,14 +501,14 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `console.log('imported');\n\n` + `var input = new URL('assets/test-ce5fc71b.ext', import.meta.url).href;\n\n` + `export default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-ce5fc71b.ext'); - assert.equal(output[1].source, 'second run'); + assert.strictEqual(output[1].fileName, 'assets/test-ce5fc71b.ext'); + assert.strictEqual(output[1].source, 'second run'); }); }); @@ -511,14 +540,14 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -537,9 +566,9 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal(runs, 2); - assert.equal(output[0].code.trim(), `alert('hello world');`); - assert.equal(output.length, 1); + assert.strictEqual(runs, 2); + assert.strictEqual(output[0].code.trim(), `alert('hello world');`); + assert.strictEqual(output.length, 1); }); }); @@ -558,14 +587,14 @@ describe('hooks', () => { const chunk = outputBundle['input.js']; // can detect that b has been tree-shaken this way - assert.equal(chunk.modules['dep'].renderedExports[0], 'a'); - assert.equal(chunk.modules['dep'].renderedExports.length, 1); + assert.strictEqual(chunk.modules['dep'].renderedExports[0], 'a'); + assert.strictEqual(chunk.modules['dep'].renderedExports.length, 1); - assert.equal(chunk.modules['dep'].removedExports[0], 'b'); - assert.equal(chunk.modules['dep'].removedExports.length, 1); + assert.strictEqual(chunk.modules['dep'].removedExports[0], 'b'); + assert.strictEqual(chunk.modules['dep'].removedExports.length, 1); - assert.equal(chunk.modules['dep'].renderedLength, 10); - assert.equal(chunk.modules['dep'].originalLength, 35); + assert.strictEqual(chunk.modules['dep'].renderedLength, 10); + assert.strictEqual(chunk.modules['dep'].originalLength, 35); } } ] @@ -629,78 +658,15 @@ describe('hooks', () => { name: 'cachePlugin', buildStart() { assert.ok(this.cache.has('asdf')); - assert.equal(this.cache.get('asdf'), 'asdf'); + assert.strictEqual(this.cache.get('asdf'), 'asdf'); + assert.strictEqual(this.cache.delete('asdf'), true); + assert.ok(!this.cache.has('asdf')); } } ] }) )); - it('throws for anonymous plugins using the cache', () => - rollup - .rollup({ - input: 'input', - plugins: [ - loader({ input: `alert('hello')` }), - { - buildStart() { - this.cache.set('asdf', 'asdf'); - } - } - ] - }) - .then(() => { - assert.fail('Should have thrown'); - }) - .catch(err => { - assert.equal(err.code, 'PLUGIN_ERROR'); - assert.equal(err.pluginCode, 'ANONYMOUS_PLUGIN_CACHE'); - })); - - it('throws for two plugins using the same name and the cache', () => { - // we don't throw for duplicate names unless there is cache access - return rollup - .rollup({ - input: 'input', - plugins: [ - loader({ input: `alert('hello')` }), - { - name: 'a' - }, - { - name: 'a' - } - ] - }) - .then(() => { - const name = 'MyTestPluginName'; - return rollup - .rollup({ - input: 'input', - plugins: [ - loader({ input: `alert('hello')` }), - { - name, - buildStart() { - this.cache.set('asdf', 'asdf'); - } - }, - { - name, - buildStart() { - this.cache.set('asdf', 'asdf'); - } - } - ] - }) - .catch(err => { - assert.equal(err.code, 'PLUGIN_ERROR'); - assert.equal(err.pluginCode, 'DUPLICATE_PLUGIN_NAME'); - assert.equal(err.message.includes(name), true); - }); - }); - }); - it('Allows plugins with any names using a shared cacheKey', () => rollup.rollup({ input: 'input', @@ -717,14 +683,14 @@ describe('hooks', () => { name: 'a', cacheKey: 'a9b6', buildEnd() { - assert.equal(this.cache.get('asdf'), 'asdf'); + assert.strictEqual(this.cache.get('asdf'), 'asdf'); } }, { name: 'b', cacheKey: 'a9b6', buildEnd() { - assert.equal(this.cache.get('asdf'), 'asdf'); + assert.strictEqual(this.cache.get('asdf'), 'asdf'); } } ] @@ -759,7 +725,7 @@ describe('hooks', () => { { name: 'x', buildStart() { - if (i === 4) assert.equal(this.cache.has('second'), true); + if (i === 4) assert.strictEqual(this.cache.has('second'), true); } } ] @@ -777,9 +743,9 @@ describe('hooks', () => { { name: 'x', buildStart() { - assert.equal(this.cache.has('first'), false); - assert.equal(this.cache.get('first'), undefined); - assert.equal(this.cache.get('second'), 'second'); + assert.strictEqual(this.cache.has('first'), false); + assert.strictEqual(this.cache.get('first'), undefined); + assert.strictEqual(this.cache.get('second'), 'second'); } } ] @@ -797,14 +763,15 @@ describe('hooks', () => { name: 'x', buildStart() { this.cache.set('x', 'x'); - assert.equal(this.cache.has('x'), false); - assert.equal(this.cache.get('x'), undefined); + assert.ok(!this.cache.has('x')); + assert.strictEqual(this.cache.get('x'), undefined); + this.cache.delete('x'); } } ] }) .then(bundle => { - assert.equal(bundle.cache, undefined); + assert.strictEqual(bundle.cache, undefined); })); it('Disables the default transform cache when using cache in transform only', () => @@ -831,7 +798,10 @@ describe('hooks', () => { { name: 'x', transform() { - assert.equal(this.cache.get('asdf'), 'asdf'); + assert.ok(this.cache.has('asdf')); + assert.strictEqual(this.cache.get('asdf'), 'asdf'); + this.cache.delete('asdf'); + assert.ok(!this.cache.has('asdf')); return `alert('hello')`; } } @@ -844,7 +814,7 @@ describe('hooks', () => { }) ) .then(({ output }) => { - assert.equal(output[0].code.trim(), `alert('hello');`); + assert.strictEqual(output[0].code.trim(), `alert('hello');`); })); it('supports renderStart hook', () => { @@ -859,13 +829,13 @@ describe('hooks', () => { { renderStart() { renderStartCount++; - assert.equal(generateBundleCount, 0); - assert.equal(renderErrorCount, 0); + assert.strictEqual(generateBundleCount, 0); + assert.strictEqual(renderErrorCount, 0); }, generateBundle() { generateBundleCount++; - assert.equal(renderStartCount, 1); - assert.equal(renderErrorCount, 0); + assert.strictEqual(renderStartCount, 1); + assert.strictEqual(renderErrorCount, 0); }, renderError() { renderErrorCount++; @@ -875,9 +845,9 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'esm' })) .then(() => { - assert.equal(renderStartCount, 1, 'renderStart count'); - assert.equal(generateBundleCount, 1, 'generateBundle count'); - assert.equal(renderErrorCount, 0, 'renderError count'); + assert.strictEqual(renderStartCount, 1, 'renderStart count'); + assert.strictEqual(generateBundleCount, 1, 'generateBundle count'); + assert.strictEqual(renderErrorCount, 0, 'renderError count'); }); }); @@ -902,8 +872,8 @@ describe('hooks', () => { }, renderError(error) { assert(error); - assert.equal(error.message, 'renderChunk error'); - assert.equal(renderStartCount, 1); + assert.strictEqual(error.message, 'renderChunk error'); + assert.strictEqual(renderStartCount, 1); renderErrorCount++; } } @@ -914,24 +884,18 @@ describe('hooks', () => { assert.ok(err); }) .then(() => { - assert.equal(renderStartCount, 1, 'renderStart count'); - assert.equal(generateBundleCount, 0, 'generateBundle count'); - assert.equal(renderErrorCount, 1, 'renderError count'); + assert.strictEqual(renderStartCount, 1, 'renderStart count'); + assert.strictEqual(generateBundleCount, 0, 'generateBundle count'); + assert.strictEqual(renderErrorCount, 1, 'renderError count'); }); }); - it('Warns when using deprecated this.watcher in plugins', () => { - let warned = false; + it('Warns once when using deprecated this.watcher in plugins', () => { + const warnings = []; const watcher = rollup.watch({ input: 'input', onwarn(warning) { - warned = true; - assert.equal(warning.code, 'PLUGIN_WARNING'); - assert.equal(warning.pluginCode, 'PLUGIN_WATCHER_DEPRECATED'); - assert.equal( - warning.message, - 'this.watcher usage is deprecated in plugins. Use the watchChange plugin hook and this.addWatchFile() instead.' - ); + warnings.push(warning); }, output: { format: 'esm' @@ -942,18 +906,95 @@ describe('hooks', () => { name: 'x', buildStart() { this.watcher.on('change', () => {}); + this.watcher.on('change', () => {}); } } ] }); return new Promise((resolve, reject) => { - watcher.on('event', evt => { - if (evt.code === 'BUNDLE_END') resolve(); - else if (evt.code === 'ERROR' || evt.code === 'FATAL') reject(evt.error); + watcher.on('event', event => { + if (event.code === 'BUNDLE_END') resolve(); + else if (event.code === 'ERROR' || event.code === 'FATAL') reject(event.error); + }); + }).catch(err => { + assert.strictEqual( + err.message, + 'You must specify "output.file" or "output.dir" for the build.' + ); + assert.strictEqual(warnings.length, 1); + const warning = warnings[0]; + assert.strictEqual(warning.code, 'PLUGIN_WARNING'); + assert.strictEqual(warning.pluginCode, 'PLUGIN_WATCHER_DEPRECATED'); + assert.strictEqual( + warning.message, + 'this.watcher usage is deprecated in plugins. Use the watchChange plugin hook and this.addWatchFile() instead.' + ); + }); + }); + + it('Throws when not specifying "file" or "dir"', () => { + const watcher = rollup.watch({ + input: 'input', + output: { + format: 'esm' + }, + plugins: [loader({ input: `alert('hello')` })] + }); + return new Promise((resolve, reject) => { + watcher.on('event', event => { + if (event.code === 'BUNDLE_END') reject(new Error('Expected an error')); + else if (event.code === 'ERROR') reject(event.error); + }); + }).catch(err => { + assert.strictEqual( + err.message, + 'You must specify "output.file" or "output.dir" for the build.' + ); + }); + }); + + it('Throws when using the "file"" option for multiple chunks', () => { + const watcher = rollup.watch({ + input: 'input', + output: { + format: 'esm', + file: 'bundle.js' + }, + plugins: [loader({ input: `import('dep')`, dep: `console.log('dep')` })] + }); + return new Promise((resolve, reject) => { + watcher.on('event', event => { + if (event.code === 'BUNDLE_END') reject(new Error('Expected an error')); + else if (event.code === 'ERROR') reject(event.error); + }); + }).catch(err => { + assert.strictEqual( + err.message, + 'You must set "output.dir" instead of "output.file" when generating multiple chunks.' + ); + }); + }); + + it('Throws when using the "sourcemapFile" option for multiple chunks', () => { + const watcher = rollup.watch({ + input: 'input', + output: { + format: 'esm', + sourcemapFile: 'bundle.map', + dir: 'ignored' + }, + plugins: [loader({ input: `import('dep')`, dep: `console.log('dep')` })] + }); + return new Promise((resolve, reject) => { + watcher.on('event', event => { + if (event.code === 'BUNDLE_END') reject(new Error('Expected an error')); + else if (event.code === 'ERROR') reject(event.error); }); }).catch(err => { - assert.equal(err.message, 'You must specify "output.file" or "output.dir" for the build.'); - assert.equal(warned, true); + assert.strictEqual( + err.message, + '"output.sourcemapFile" is only supported for single-file builds.' + ); }); }); @@ -1040,7 +1081,7 @@ describe('hooks', () => { }) ) .then(output => { - assert.equal(augmentChunkHashCalls, 1); + assert.strictEqual(augmentChunkHashCalls, 1); }); }); @@ -1054,14 +1095,14 @@ describe('hooks', () => { onwarn(warning) { deprecationCnt++; if (deprecationCnt === 1) { - assert.equal(warning.code, 'DEPRECATED_FEATURE'); - assert.equal( + assert.strictEqual(warning.code, 'DEPRECATED_FEATURE'); + assert.strictEqual( warning.message, 'The "ongenerate" hook used by plugin at position 2 is deprecated. The "generateBundle" hook should be used instead.' ); } else { - assert.equal(warning.code, 'DEPRECATED_FEATURE'); - assert.equal( + assert.strictEqual(warning.code, 'DEPRECATED_FEATURE'); + assert.strictEqual( warning.message, 'The "onwrite" hook used by plugin at position 2 is deprecated. The "generateBundle/writeBundle" hook should be used instead.' ); @@ -1075,7 +1116,7 @@ describe('hooks', () => { }, onwrite(bundle, out) { - assert.equal(out.ongenerate, true); + assert.strictEqual(out.ongenerate, true); } } ] @@ -1087,7 +1128,7 @@ describe('hooks', () => { }) ) .then(() => { - assert.equal(deprecationCnt, 2); + assert.strictEqual(deprecationCnt, 2); return sander.rimraf(TEMP_DIR); }); }); @@ -1164,8 +1205,8 @@ describe('hooks', () => { input: 'input', onwarn(warning) { deprecationCnt++; - assert.equal(warning.code, 'DEPRECATED_FEATURE'); - assert.equal( + assert.strictEqual(warning.code, 'DEPRECATED_FEATURE'); + assert.strictEqual( warning.message, 'The "transformChunk" hook used by plugin at position 2 is deprecated. The "renderChunk" hook should be used instead.' ); @@ -1178,7 +1219,7 @@ describe('hooks', () => { try { this.emitAsset('test.ext', 'hello world'); } catch (e) { - assert.equal(e.code, 'ASSETS_ALREADY_FINALISED'); + assert.strictEqual(e.code, 'ASSETS_ALREADY_FINALISED'); } } } @@ -1191,8 +1232,8 @@ describe('hooks', () => { }) ) .then(() => { - assert.equal(deprecationCnt, 1); - assert.equal(calledHook, true); + assert.strictEqual(deprecationCnt, 1); + assert.strictEqual(calledHook, true); }); }); @@ -1208,8 +1249,8 @@ describe('hooks', () => { return `export default import.meta.ROLLUP_ASSET_URL_${assetId};`; }, generateBundle(options, outputBundle, isWrite) { - assert.equal(outputBundle['assets/test-0a676135.ext'].source, 'hello world'); - assert.equal( + assert.strictEqual(outputBundle['assets/test-0a676135.ext'].source, 'hello world'); + assert.strictEqual( outputBundle['input.js'].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); @@ -1240,12 +1281,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); return rollup.rollup({ cache, @@ -1265,12 +1306,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); return rollup.rollup({ cache, @@ -1287,12 +1328,12 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); }); }); @@ -1316,14 +1357,14 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -1343,14 +1384,14 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -1367,14 +1408,14 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); }); }); @@ -1402,14 +1443,14 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -1428,9 +1469,9 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal(runs, 2); - assert.equal(output[0].code.trim(), `alert('hello world');`); - assert.equal(output.length, 1); + assert.strictEqual(runs, 2); + assert.strictEqual(output[0].code.trim(), `alert('hello world');`); + assert.strictEqual(output.length, 1); }); }); @@ -1454,7 +1495,7 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output: [, output] }) => { - assert.equal(output.source, 'hello world'); + assert.strictEqual(output.source, 'hello world'); }); }); @@ -1478,8 +1519,8 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output: [, output] }) => { - assert.equal(output.fileName, 'assets/test-0a676135.ext'); - assert.equal(output.source, 'hello world'); + assert.strictEqual(output.fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output.source, 'hello world'); }); }); }); diff --git a/test/misc/optionList.js b/test/misc/optionList.js index 4a28a584e70..17211adb8b5 100644 --- a/test/misc/optionList.js +++ b/test/misc/optionList.js @@ -1,3 +1,3 @@ exports.input = 'acorn, acornInjectPlugins, cache, chunkGroupingSize, context, experimentalCacheExpiry, experimentalOptimizeChunks, experimentalTopLevelAwait, external, inlineDynamicImports, input, manualChunks, moduleContext, onwarn, perf, plugins, preserveModules, preserveSymlinks, shimMissingExports, strictDeprecations, treeshake, watch'; exports.flags = 'acorn, acornInjectPlugins, amd, assetFileNames, banner, c, cache, chunkFileNames, chunkGroupingSize, compact, config, context, d, dir, dynamicImportFunction, e, entryFileNames, environment, esModule, experimentalCacheExpiry, experimentalOptimizeChunks, experimentalTopLevelAwait, exports, extend, external, externalLiveBindings, f, file, footer, format, freeze, g, globals, h, i, indent, inlineDynamicImports, input, interop, intro, m, manualChunks, moduleContext, n, name, namespaceToStringTag, noConflict, o, onwarn, outro, paths, perf, plugins, preferConst, preserveModules, preserveSymlinks, shimMissingExports, silent, sourcemap, sourcemapExcludeSources, sourcemapFile, strict, strictDeprecations, treeshake, v, w, watch'; -exports.output = 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, entryFileNames, esModule, exports, extend, externalLiveBindings, file, footer, format, freeze, globals, indent, interop, intro, name, namespaceToStringTag, noConflict, outro, paths, preferConst, sourcemap, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict'; +exports.output = 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, entryFileNames, esModule, exports, extend, externalLiveBindings, file, footer, format, freeze, globals, indent, interop, intro, name, namespaceToStringTag, noConflict, outro, paths, plugins, preferConst, sourcemap, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict';