Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

augmentChunkHash plugin hook #2921

Merged
merged 16 commits into from Aug 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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 {
isidrok marked this conversation as resolved.
Show resolved Hide resolved
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', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be interesting to convert some of those tests to file-hashes tests, i.e. tests/file-hashes/samples/.... These are specifically designed to compare two different setups and throw when there are files with the same hashes but different content. Also, those tests are closer to real world scenarios.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think this is too important to test as calling is implicitly tested with any test that also tests that the hash is changed, which is what is actually important to us.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the end I left this one inside hook tests and created a file-hashes one with tests the rest of them

});
});

describe('deprecated', () => {
it('passes bundle & output object to ongenerate & onwrite hooks, with deprecation warnings', () => {
let deprecationCnt = 0;
Expand Down