diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index f51b587747f..e42309a8ad3 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -72,6 +72,23 @@ 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 +#### `augmentChunkHash` +Type: `(preRenderedChunk: PreRenderedChunk) => string`
+Kind: `sync, sequential` + +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. + +The following plugin will invalidate the hash of chunk `foo` with the timestamp of the last build: + +```javascript +// rollup.config.js +augmentChunkHash(chunkInfo) { + if(chunkInfo.name === 'foo') { + return Date.now(); + } +} +``` + #### `banner` Type: `string | (() => string)`
Kind: `async, parallel` @@ -257,7 +274,7 @@ Kind: `sync, first` 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. By default for formats other than ES modules, Rollup replaces `import.meta.url` with code that attempts to match this behaviour by returning the dynamic URL of the current chunk. Note that all formats except CommonJS and UMD assume that they run in a browser environment where `URL` and `document` are available. For other properties, `import.meta.someProperty` is replaced with `undefined` while `import.meta` is replaced with an object containing a `url` property. - + This behaviour can be changed—also for ES modules—via this hook. For each occurrence of `import.meta<.someProperty>`, this hook is called with the name of the property or `null` if `import.meta` is accessed directly. For example, the following code will resolve `import.meta.url` using the relative path of the original module to the current working directory and again resolve this path against the base URL of the current document at runtime: ```javascript @@ -352,9 +369,9 @@ Emits a new file that is included in the build output and returns a `referenceId ``` In both cases, either a `name` or a `fileName` can be supplied. If a `fileName` is provided, it will be used unmodified as the name of the generated file, throwing an error if this causes a conflict. Otherwise if a `name` is supplied, this will be used as substitution for `[name]` in the corresponding [`output.chunkFileNames`](guide/en/#outputchunkfilenames) or [`output.assetFileNames`](guide/en/#outputassetfilenames) pattern, possibly adding a unique number to the end of the file name to avoid conflicts. If neither a `name` nor `fileName` is supplied, a default name will be used. - + You can reference the URL of an emitted file in any code returned by a [`load`](guide/en/#load) or [`transform`](guide/en/#transform) plugin hook via `import.meta.ROLLUP_FILE_URL_referenceId`. See [File URLs](guide/en/#file-urls) for more details and an example. - + The generated code that replaces `import.meta.ROLLUP_FILE_URL_referenceId` can be customized via the [`resolveFileUrl`](guide/en/#resolvefileurl) plugin hook. You can also use [`this.getFileName(referenceId)`](guide/en/#thisgetfilenamereferenceid-string--string) to determine the file name as soon as it is available If the `type` is *`chunk`*, then this emits a new chunk with the given module id as entry point. This will not result in duplicate modules in the graph, instead if necessary, existing chunks will be split or a facade chunk with reexports will be created. Chunks with a specified `fileName` will always generate separate chunks while other emitted chunks may be deduplicated with existing chunks even if the `name` does not match. If such a chunk is not deduplicated, the [`output.chunkFileNames`](guide/en/#outputchunkfilenames) name pattern will be used. @@ -437,15 +454,15 @@ The `position` argument is a character index where the warning was raised. If pr ☢️ These context utility functions have been deprecated and may be removed in a future Rollup version. - `this.emitAsset(assetName: string, source: string) => string` - _**Use [`this.emitFile`](guide/en/#thisemitfileemittedfile-emittedchunk--emittedasset--string)**_ - Emits a custom file that is included in the build output, returning an `assetReferenceId` that can be used to reference the emitted file. You can defer setting the source if you provide it later via [`this.setAssetSource(assetReferenceId, source)`](guide/en/#thissetassetsourceassetreferenceid-string-source-string--buffer--void). A string or Buffer source must be set for each asset through either method or an error will be thrown on generate completion. - + Emitted assets will follow the [`output.assetFileNames`](guide/en/#outputassetfilenames) naming scheme. You can reference the URL of the file in any code returned by a [`load`](guide/en/#load) or [`transform`](guide/en/#transform) plugin hook via `import.meta.ROLLUP_ASSET_URL_assetReferenceId`. - + The generated code that replaces `import.meta.ROLLUP_ASSET_URL_assetReferenceId` can be customized via the [`resolveFileUrl`](guide/en/#resolvefileurl) plugin hook. Once the asset has been finalized during `generate`, you can also use [`this.getFileName(assetReferenceId)`](guide/en/#thisgetfilenamereferenceid-string--string) to determine the file name. - `this.emitChunk(moduleId: string, options?: {name?: string}) => string` - _**Use [`this.emitFile`](guide/en/#thisemitfileemittedfile-emittedchunk--emittedasset--string)**_ - Emits a new chunk with the given module as entry point. This will not result in duplicate modules in the graph, instead if necessary, existing chunks will be split. It returns a `chunkReferenceId` that can be used to later access the generated file name of the chunk. - + Emitted chunks will follow the [`output.chunkFileNames`](guide/en/#outputchunkfilenames), [`output.entryFileNames`](guide/en/#outputentryfilenames) naming scheme. If a `name` is provided, this will be used for the `[name]` file name placeholder, otherwise the name will be derived from the file name. If a `name` is provided, this name must not conflict with any other entry point names unless the entry points reference the same entry module. You can reference the URL of the emitted chunk in any code returned by a [`load`](guide/en/#load) or [`transform`](guide/en/#transform) plugin hook via `import.meta.ROLLUP_CHUNK_URL_chunkReferenceId`. - + The generated code that replaces `import.meta.ROLLUP_CHUNK_URL_chunkReferenceId` can be customized via the [`resolveFileUrl`](guide/en/#resolvefileurl) plugin hook. Once the chunk has been rendered during `generate`, you can also use [`this.getFileName(chunkReferenceId)`](guide/en/#thisgetfilenamereferenceid-string--string) to determine the file name. - `this.getAssetFileName(assetReferenceId: string) => string` - _**Use [`this.getFileName`](guide/en/#thisgetfilenamereferenceid-string--string)**_ - Get the file name of an asset, according to the `assetFileNames` output option pattern. The file name will be relative to `outputOptions.dir`. @@ -494,7 +511,7 @@ image.src = logo; document.body.appendChild(image); ``` -Similar to assets, emitted chunks can be referenced from within JS code via `import.meta.ROLLUP_FILE_URL_referenceId` as well. +Similar to assets, emitted chunks can be referenced from within JS code via `import.meta.ROLLUP_FILE_URL_referenceId` as well. The following example will detect imports prefixed with `register-paint-worklet:` and generate the necessary code and separate chunk to generate a CSS paint worklet. Note that this will only work in modern browsers and will only work if the output format is set to `esm`. diff --git a/package-lock.json b/package-lock.json index 20fa789d91f..90e5edd7465 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1023,8 +1023,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "console-group": { "version": "0.3.3", @@ -2229,8 +2228,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -2259,7 +2257,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3848,7 +3845,6 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6317,8 +6313,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true, - "optional": true + "dev": true }, "yargs": { "version": "13.2.2", diff --git a/src/Chunk.ts b/src/Chunk.ts index c751139466b..b07ef826801 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -18,6 +18,7 @@ import { DecodedSourceMapOrMissing, GlobalsOption, OutputOptions, + PreRenderedChunk, RenderedChunk, RenderedModule } from './rollup/types'; @@ -341,6 +342,8 @@ export default class Chunk { if (this.renderedHash) return this.renderedHash; if (!this.renderedSource) return ''; const hash = sha256(); + const hashAugmentation = this.calculateHashAugmentation(); + hash.update(hashAugmentation); hash.update(this.renderedSource.toString()); hash.update( this.getExportNames() @@ -799,9 +802,37 @@ export default class Chunk { } } + private calculateHashAugmentation(): string { + const facadeModule = this.facadeModule; + const getChunkName = this.getChunkName.bind(this); + const preRenderedChunk = { + dynamicImports: this.getDynamicImportIds(), + exports: this.getExportNames(), + facadeModuleId: facadeModule && facadeModule.id, + imports: this.getImportIds(), + isDynamicEntry: facadeModule !== null && facadeModule.dynamicallyImportedBy.length > 0, + isEntry: facadeModule !== null && facadeModule.isEntryPoint, + modules: this.renderedModules, + get name() { + return getChunkName(); + } + } as PreRenderedChunk; + const hashAugmentation = this.graph.pluginDriver.hookReduceValueSync( + 'augmentChunkHash', + '', + [preRenderedChunk], + (hashAugmentation, pluginHash) => { + if (pluginHash) { + hashAugmentation += pluginHash; + } + return hashAugmentation; + } + ); + return hashAugmentation; + } + private computeContentHashWithDependencies(addons: Addons, options: OutputOptions): string { const hash = sha256(); - hash.update( [addons.intro, addons.outro, addons.banner, addons.footer].map(addon => addon || '').join(':') ); diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 41d9b5c82bc..f24e305b4d7 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -327,6 +327,7 @@ interface OnWriteOptions extends OutputOptions { } export interface PluginHooks { + augmentChunkHash: (this: PluginContext, chunk: PreRenderedChunk) => string | void; buildEnd: (this: PluginContext, err?: Error) => Promise | void; buildStart: (this: PluginContext, options: InputOptions) => Promise | void; generateBundle: ( @@ -499,11 +500,10 @@ export interface RenderedModule { renderedLength: number; } -export interface RenderedChunk { +export interface PreRenderedChunk { dynamicImports: string[]; exports: string[]; facadeModuleId: string | null; - fileName: string; imports: string[]; isDynamicEntry: boolean; isEntry: boolean; @@ -513,6 +513,10 @@ export interface RenderedChunk { name: string; } +export interface RenderedChunk extends PreRenderedChunk { + fileName: string; +} + export interface OutputChunk extends RenderedChunk { code: string; map?: SourceMap; diff --git a/src/utils/pluginDriver.ts b/src/utils/pluginDriver.ts index cf6411c53cc..3446b8622b8 100644 --- a/src/utils/pluginDriver.ts +++ b/src/utils/pluginDriver.ts @@ -63,6 +63,13 @@ export interface PluginDriver { reduce: Reduce, hookContext?: HookContext ): Promise; + hookReduceValueSync( + hook: string, + value: T, + args: any[], + reduce: Reduce, + hookContext?: HookContext + ): T; hookSeq( hook: H, args: Args, @@ -466,6 +473,16 @@ export function createPluginDriver( 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); } diff --git a/test/file-hashes/samples/augment-chunk-hash/_config.js b/test/file-hashes/samples/augment-chunk-hash/_config.js new file mode 100644 index 00000000000..4cc7c0f564e --- /dev/null +++ b/test/file-hashes/samples/augment-chunk-hash/_config.js @@ -0,0 +1,49 @@ +const augment1 = '/*foo*/'; +const augment2 = '/*bar*/'; +module.exports = { + description: 'augmentChunkHash updates hashes across all modules when returning something', + options1: { + input: 'main', + output: { + format: 'esm', + entryFileNames: '[name]-[hash].js', + chunkFileNames: '[name]-[hash].js' + }, + plugins: [ + { + augmentChunkHash(chunk) { + if (chunk.name === 'main') { + return augment1; + } + }, + renderChunk(code, chunk) { + if (chunk.name === 'main') { + return augment1 + code; + } + } + } + ] + }, + options2: { + input: 'main', + output: { + format: 'esm', + entryFileNames: '[name]-[hash].js', + chunkFileNames: '[name]-[hash].js' + }, + plugins: [ + { + augmentChunkHash(chunk) { + if (chunk.name === 'main') { + return augment2; + } + }, + renderChunk(code, chunk) { + if (chunk.name === 'main') { + return augment2 + code; + } + } + } + ] + } +}; diff --git a/test/file-hashes/samples/augment-chunk-hash/main.js b/test/file-hashes/samples/augment-chunk-hash/main.js new file mode 100644 index 00000000000..c0b933d7b56 --- /dev/null +++ b/test/file-hashes/samples/augment-chunk-hash/main.js @@ -0,0 +1 @@ +console.log('main'); diff --git a/test/hooks/index.js b/test/hooks/index.js index 658b7ce8280..1ecff4f0602 100644 --- a/test/hooks/index.js +++ b/test/hooks/index.js @@ -1014,6 +1014,36 @@ describe('hooks', () => { }); }); + it('supports augmentChunkHash hook', () => { + let augmentChunkHashCalls = 0; + return rollup + .rollup({ + input: 'input', + plugins: [ + loader({ + input: `alert('hello')` + }), + { + augmentChunkHash(update) { + augmentChunkHashCalls++; + assert(this.meta); + assert(this.meta.rollupVersion); + } + } + ] + }) + .then(bundle => + bundle.generate({ + format: 'esm', + dir: 'dist', + entryFileNames: '[name]-[hash].js' + }) + ) + .then(output => { + assert.equal(augmentChunkHashCalls, 1); + }); + }); + describe('deprecated', () => { it('passes bundle & output object to ongenerate & onwrite hooks, with deprecation warnings', () => { let deprecationCnt = 0;