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
Changes from 5 commits
5561b69
55bbad4
3550bde
1657402
49a1670
52ae7ff
cb4475d
35053bd
237254c
b9ad5d1
e2aac9a
d4dcc58
5800c03
373f6e9
57ccc8c
74f8305
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,6 +124,7 @@ export default class Chunk { | |
return chunk; | ||
} | ||
|
||
augmentedHash?: string; | ||
entryModules: Module[] = []; | ||
execIndex: number; | ||
exportMode = 'named'; | ||
|
@@ -325,6 +326,9 @@ export default class Chunk { | |
if (this.renderedHash) return this.renderedHash; | ||
if (!this.renderedSource) return ''; | ||
const hash = sha256(); | ||
if (this.augmentedHash) { | ||
hash.update(this.augmentedHash); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the |
||
} | ||
hash.update(this.renderedSource.toString()); | ||
hash.update( | ||
this.getExportNames() | ||
|
@@ -799,7 +803,6 @@ export default class Chunk { | |
|
||
private computeContentHashWithDependencies(addons: Addons, options: OutputOptions): string { | ||
const hash = sha256(); | ||
|
||
hash.update( | ||
[addons.intro, addons.outro, addons.banner, addons.footer].map(addon => addon || '').join(':') | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ import { | |
OutputOptions, | ||
Plugin, | ||
PluginContext, | ||
PreRenderedChunk, | ||
RollupBuild, | ||
RollupCache, | ||
RollupOutput, | ||
|
@@ -211,45 +212,81 @@ export default function rollup(rawInputOptions: GenericConfigObject): Promise<Ro | |
optimized = true; | ||
} | ||
|
||
assignChunkIds(chunks, inputOptions, outputOptions, inputBase, addons); | ||
|
||
// assign to outputBundle | ||
for (let i = 0; i < chunks.length; i++) { | ||
const chunk = chunks[i]; | ||
const facadeModule = chunk.facadeModule; | ||
|
||
outputBundle[chunk.id] = { | ||
code: undefined as any, | ||
dynamicImports: chunk.getDynamicImportIds(), | ||
exports: chunk.getExportNames(), | ||
facadeModuleId: facadeModule && facadeModule.id, | ||
fileName: chunk.id, | ||
imports: chunk.getImportIds(), | ||
isDynamicEntry: | ||
facadeModule !== null && facadeModule.dynamicallyImportedBy.length > 0, | ||
isEntry: facadeModule !== null && facadeModule.isEntryPoint, | ||
map: undefined, | ||
modules: chunk.renderedModules, | ||
get name() { | ||
return chunk.getChunkName(); | ||
} | ||
} as OutputChunk; | ||
} | ||
|
||
return Promise.all( | ||
chunks.map(chunk => { | ||
const outputChunk = outputBundle[chunk.id] 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 | ||
]); | ||
}); | ||
const facadeModule = chunk.facadeModule; | ||
const chunkInfo = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not like calculating this information twice. Besides performance considerations, this information will need to be maintained and synced in more than one place which is always problematic. One way forward might be to first create a Map of Chunk -> PreRenderedChunk info and use this map in the second usage. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, could also do it in an array and just match positions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that would be fine as well. |
||
dynamicImports: chunk.getDynamicImportIds(), | ||
exports: chunk.getExportNames(), | ||
facadeModuleId: facadeModule && facadeModule.id, | ||
imports: chunk.getImportIds(), | ||
isDynamicEntry: | ||
facadeModule !== null && facadeModule.dynamicallyImportedBy.length > 0, | ||
isEntry: facadeModule !== null && facadeModule.isEntryPoint, | ||
modules: chunk.renderedModules, | ||
get name() { | ||
return chunk.getChunkName(); | ||
} | ||
} as PreRenderedChunk; | ||
return graph.pluginDriver | ||
.hookReduceValue( | ||
'augmentChunkHash', | ||
'', | ||
[chunkInfo], | ||
(hashForChunk, pluginHash) => { | ||
if (pluginHash) { | ||
hashForChunk += pluginHash; | ||
} | ||
return hashForChunk; | ||
} | ||
) | ||
.then(hashForChunk => { | ||
if (hashForChunk) { | ||
chunk.augmentedHash = hashForChunk; | ||
} | ||
}); | ||
}) | ||
).then(() => {}); | ||
).then(() => { | ||
assignChunkIds(chunks, inputOptions, outputOptions, inputBase, addons); | ||
|
||
// assign to outputBundle | ||
for (let i = 0; i < chunks.length; i++) { | ||
const chunk = chunks[i]; | ||
const facadeModule = chunk.facadeModule; | ||
|
||
outputBundle[chunk.id] = { | ||
code: undefined as any, | ||
dynamicImports: chunk.getDynamicImportIds(), | ||
exports: chunk.getExportNames(), | ||
facadeModuleId: facadeModule && facadeModule.id, | ||
fileName: chunk.id, | ||
imports: chunk.getImportIds(), | ||
isDynamicEntry: | ||
facadeModule !== null && facadeModule.dynamicallyImportedBy.length > 0, | ||
isEntry: facadeModule !== null && facadeModule.isEntryPoint, | ||
map: undefined, | ||
modules: chunk.renderedModules, | ||
get name() { | ||
return chunk.getChunkName(); | ||
} | ||
} as OutputChunk; | ||
} | ||
|
||
return Promise.all( | ||
chunks.map(chunk => { | ||
const outputChunk = outputBundle[chunk.id] 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 | ||
]); | ||
}); | ||
}) | ||
).then(() => {}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure what exactly causes the test failures, e.g. misc/'handles different import paths for different outputs', but it is definitely caused by some of the changes in this file. Maybe it becomes clearer once the code is deduplicated. My guess is that there is something that has a side effect that does not look side-effectful here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have found what's causing the errors and its pretty strange: return graph.pluginDriver
.hookParallel('renderStart', [])
.then(() => createAddons(graph, outputOptions))
.then(addons => {
// ...
return Promise.all(
chunks.map(chunk => {
const outputChunk = outputBundle[chunk.id] as OutputChunk;
return chunk.render(outputOptions, addons, outputChunk).then(rendered => {
// ...
return graph.pluginDriver.hookParallel('ongenerate', [
{ bundle: outputChunk, ...outputOptions },
outputChunk
]);
});
})
).then(() => {});
}) Works fine and test pass whereas if I move the return graph.pluginDriver
.hookParallel('renderStart', [])
.then(() => createAddons(graph, outputOptions))
.then(addons => {
// ...
const augmentChunkHashes = Promise.resolve();
return augmentChunkHash.then( () => {
// Moving this Promise.all inside a then breaks test
return Promise.all(
chunks.map(chunk => {
const outputChunk = outputBundle[chunk.id] as OutputChunk;
return chunk.render(outputOptions, addons, outputChunk).then(rendered => {
// ...
return graph.pluginDriver.hookParallel('ongenerate', [
{ bundle: outputChunk, ...outputOptions },
outputChunk
]);
});
})
).then(() => {});
})
}) Some tests break but if I understand correctly: Promise.all(promises)
.then(()=>{})
===
Promise.resolve()
.then(()=>Promise.all(promises).then(()=>{})) Any idea? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a slight difference in that errors thrown synchronously when generating the promises will be caught and reject the Promise in the second case while in the first case, they will be thrown synchronously. But I do not think this is the issue here. I cannot seem to be able to confirm your finding, will do some digging myself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also in the second case, execution will be one micro-tick slower, which would point to a possible race condition. But I really hope this is not the issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I understand the issue now. It could be argued that this is a bug in Rollup but it is actually only a bug with your modification. The state only needs to be preserved between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly not sure what a good solution is except forbidding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for looking into this, I was loosing my mind. So if I get it right the correct order of events would be to Another possible solution could be running this hook before |
||
}) | ||
.catch(error => | ||
graph.pluginDriver.hookParallel('renderError', [error]).then(() => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1328,4 +1328,157 @@ module.exports = input; | |
]); | ||
}); | ||
}); | ||
it('supports augmentChunkHash hook', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
}); | ||
}); | ||
it('augments the chunk hash when the update function is called within the augmentChunkHash hook', () => { | ||
const inputCode = `alert('hello')`; | ||
const outputOptions = { | ||
format: 'esm', | ||
dir: 'dist', | ||
entryFileNames: '[name]-[hash].js' | ||
}; | ||
function getFileName(bundle) { | ||
return bundle.generate(outputOptions).then(({ output }) => { | ||
return output[0].fileName; | ||
}); | ||
} | ||
function bundleWithoutAugment() { | ||
return rollup | ||
.rollup({ | ||
input: 'input', | ||
plugins: [ | ||
loader({ | ||
input: inputCode | ||
}) | ||
] | ||
}) | ||
.then(getFileName); | ||
} | ||
function bundleWithAugment() { | ||
return rollup | ||
.rollup({ | ||
input: 'input', | ||
plugins: [ | ||
loader({ | ||
input: inputCode | ||
}), | ||
{ | ||
augmentChunkHash() { | ||
return 'foo'; | ||
} | ||
} | ||
] | ||
}) | ||
.then(getFileName); | ||
} | ||
return Promise.all([bundleWithoutAugment(), bundleWithAugment()]).then(([base, augmented]) => { | ||
assert.notEqual(base, augmented); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test is a prime candidate for being converted to a |
||
}); | ||
it('propagates hash updates of augmentChunkHash to all dependents', () => { | ||
return rollup | ||
.rollup({ | ||
input: 'input', | ||
plugins: [ | ||
loader({ | ||
input: `console.log('input');import('other');`, | ||
other: `console.log('other');` | ||
}), | ||
{ | ||
augmentChunkHash() { | ||
return Promise.resolve('foo'); | ||
} | ||
} | ||
] | ||
}) | ||
.then(bundle => | ||
bundle.generate({ | ||
format: 'esm', | ||
dir: 'dist', | ||
entryFileNames: '[name]-[hash].js', | ||
chunkFileNames: '[name]-[hash].js' | ||
}) | ||
) | ||
.then(({ output }) => { | ||
const importedFileName = output[0].dynamicImports[0]; | ||
const otherFileName = output[1].fileName; | ||
assert.equal(otherFileName, importedFileName); | ||
}); | ||
}); | ||
it('augmentChunkHash only takes effect for chunks whose call got a return value', () => { | ||
const outputOptions = { | ||
format: 'esm', | ||
dir: 'dist', | ||
entryFileNames: '[name]-[hash].js' | ||
}; | ||
const input = ['input', 'other']; | ||
const inputCode = { | ||
input: `console.log('input');`, | ||
other: `console.log('other');` | ||
}; | ||
function getFileNamesForChunks(bundle) { | ||
return bundle.generate(outputOptions).then(({ output }) => { | ||
return output.reduce((result, chunk) => { | ||
result[chunk.name] = chunk.fileName; | ||
return result; | ||
}, {}); | ||
}); | ||
} | ||
function bundleWithoutAugment() { | ||
return rollup | ||
.rollup({ | ||
input, | ||
plugins: [loader(inputCode)] | ||
}) | ||
.then(getFileNamesForChunks); | ||
} | ||
function bundleWithAugment() { | ||
return rollup | ||
.rollup({ | ||
input, | ||
plugins: [ | ||
loader(inputCode), | ||
{ | ||
augmentChunkHash(chunk) { | ||
if (chunk.name === 'input') { | ||
return 'foo'; | ||
} | ||
} | ||
} | ||
] | ||
}) | ||
.then(getFileNamesForChunks); | ||
} | ||
return Promise.all([bundleWithoutAugment(), bundleWithAugment()]).then(([base, augmented]) => { | ||
assert.notEqual(base.input, augmented.input); | ||
assert.equal(base.other, augmented.other); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From how this is generated, I would say this is more like a
hashAugmentation
as the actual hash is composed of more things. But maybe we can get rid of this property, cf. my other comments?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense and will actually accomplish the lazy behaviour