Skip to content

Commit

Permalink
augmentChunkHash plugin hook (#2921)
Browse files Browse the repository at this point in the history
* test: augmentChunkHash plugin hook tests

* feat: augmentChunkHash plugin hook

Hook to extend chunk hashes callable from within plugins in order to take in to account implicit dependencies such as plugin options or version when creating the chunk hash

* test: independent hashing in augmentChunkHash tests

* feat: pass reduced chunk info to ugmentChunkHash hook and hash chunks independenly

* augmentChunkHash remove redundant test

* augmentChunkHash fix typings

* move augmentChungHash to chunk

* restore unchanged files

* allow augmentChunk to return string or void

* update augment-chunk-hash tests

* update augment-chunk-hash tests
  • Loading branch information
isidrok authored and lukastaegert committed Aug 13, 2019
1 parent 04b7d52 commit d4f7736
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 19 deletions.
33 changes: 25 additions & 8 deletions docs/05-plugin-development.md
Expand Up @@ -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`<br>
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)`<br>
Kind: `async, parallel`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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`.

Expand Down
11 changes: 3 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 32 additions & 1 deletion src/Chunk.ts
Expand Up @@ -18,6 +18,7 @@ import {
DecodedSourceMapOrMissing,
GlobalsOption,
OutputOptions,
PreRenderedChunk,
RenderedChunk,
RenderedModule
} from './rollup/types';
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(':')
);
Expand Down
8 changes: 6 additions & 2 deletions src/rollup/types.d.ts
Expand Up @@ -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> | void;
buildStart: (this: PluginContext, options: InputOptions) => Promise<void> | void;
generateBundle: (
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions src/utils/pluginDriver.ts
Expand Up @@ -63,6 +63,13 @@ export interface PluginDriver {
reduce: Reduce<R, T>,
hookContext?: HookContext
): Promise<T>;
hookReduceValueSync<R = any, T = any>(
hook: string,
value: T,
args: any[],
reduce: Reduce<R, T>,
hookContext?: HookContext
): T;
hookSeq<H extends keyof PluginHooks>(
hook: H,
args: Args<PluginHooks[H]>,
Expand Down Expand Up @@ -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);
}
Expand Down
49 changes: 49 additions & 0 deletions 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;
}
}
}
]
}
};
1 change: 1 addition & 0 deletions test/file-hashes/samples/augment-chunk-hash/main.js
@@ -0,0 +1 @@
console.log('main');
30 changes: 30 additions & 0 deletions test/hooks/index.js
Expand Up @@ -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;
Expand Down

0 comments on commit d4f7736

Please sign in to comment.