Skip to content

Commit 4d848c4

Browse files
martinfrancoisdgp1130
authored andcommitted
fix(@angular-devkit/build-angular): generate different content hashes for scripts which are changed during the optimization phase
Instead of generating the content hash based on the content of scripts BEFORE the optimization phase, the content hash is generated AFTER the optimization phase. Prevents caching issues where browsers block execution of scripts due to the integrity hash not matching with the cached script in case of a script being optimized differently than in a previous build, where it would previously result in the same content hash. Fixes #22906 (cherry picked from commit 357c45e)
1 parent 10f2449 commit 4d848c4

File tree

2 files changed

+74
-11
lines changed

2 files changed

+74
-11
lines changed

packages/angular_devkit/build_angular/src/webpack/plugins/scripts-webpack-plugin.ts

+29-11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import { Chunk, Compilation, Compiler, sources as webpackSources } from 'webpack
1212

1313
const Entrypoint = require('webpack/lib/Entrypoint');
1414

15+
/**
16+
* The name of the plugin provided to Webpack when tapping Webpack compiler hooks.
17+
*/
18+
const PLUGIN_NAME = 'scripts-webpack-plugin';
19+
1520
export interface ScriptsWebpackPluginOptions {
1621
name: string;
1722
sourceMap?: boolean;
@@ -97,8 +102,8 @@ export class ScriptsWebpackPlugin {
97102
.filter((script) => !!script)
98103
.map((script) => path.resolve(this.options.basePath || '', script));
99104

100-
compiler.hooks.thisCompilation.tap('scripts-webpack-plugin', (compilation) => {
101-
compilation.hooks.additionalAssets.tapPromise('scripts-webpack-plugin', async () => {
105+
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
106+
compilation.hooks.additionalAssets.tapPromise(PLUGIN_NAME, async () => {
102107
if (await this.shouldSkip(compilation, scripts)) {
103108
if (this._cachedOutput) {
104109
this._insertOutput(compilation, this._cachedOutput, true);
@@ -149,19 +154,32 @@ export class ScriptsWebpackPlugin {
149154
});
150155

151156
const combinedSource = new webpackSources.CachedSource(concatSource);
152-
const filename = interpolateName(
153-
{ resourcePath: 'scripts.js' },
154-
this.options.filename as string,
155-
{
156-
content: combinedSource.source(),
157-
},
158-
);
159-
160-
const output = { filename, source: combinedSource };
157+
158+
const output = { filename: this.options.filename, source: combinedSource };
161159
this._insertOutput(compilation, output);
162160
this._cachedOutput = output;
163161
addDependencies(compilation, scripts);
164162
});
163+
compilation.hooks.processAssets.tapPromise(
164+
{
165+
name: PLUGIN_NAME,
166+
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING,
167+
},
168+
async () => {
169+
const assetName = this.options.filename;
170+
const asset = compilation.getAsset(assetName);
171+
if (asset) {
172+
const interpolatedFilename = interpolateName(
173+
{ resourcePath: 'scripts.js' },
174+
assetName,
175+
{ content: asset.source.source() },
176+
);
177+
if (assetName !== interpolatedFilename) {
178+
compilation.renameAsset(assetName, interpolatedFilename);
179+
}
180+
}
181+
},
182+
);
165183
});
166184
}
167185
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expectFileMatchToExist, expectFileToMatch, writeMultipleFiles } from '../../utils/fs';
2+
import { ng } from '../../utils/process';
3+
import { updateJsonFile, updateTsConfig } from '../../utils/project';
4+
5+
function getScriptsFilename(): Promise<string> {
6+
return expectFileMatchToExist('dist/test-project/', /external-module\.[0-9a-f]{16}\.js/);
7+
}
8+
9+
export default async function () {
10+
// verify content hash is based on code after optimizations
11+
await writeMultipleFiles({
12+
'src/script.js': 'try { console.log(); } catch {}',
13+
});
14+
await updateJsonFile('angular.json', (configJson) => {
15+
const build = configJson.projects['test-project'].architect.build;
16+
build.options['scripts'] = [
17+
{
18+
input: 'src/script.js',
19+
inject: true,
20+
bundleName: 'external-module',
21+
},
22+
];
23+
build.configurations['production'].outputHashing = 'all';
24+
configJson['cli'] = { cache: { enabled: 'false' } };
25+
});
26+
await updateTsConfig((json) => {
27+
json['compilerOptions']['target'] = 'es2017';
28+
json['compilerOptions']['module'] = 'es2020';
29+
});
30+
await ng('build', '--configuration=production');
31+
const filenameBuild1 = await getScriptsFilename();
32+
await expectFileToMatch(`dist/test-project/${filenameBuild1}`, 'try{console.log()}catch(c){}');
33+
34+
await updateTsConfig((json) => {
35+
json['compilerOptions']['target'] = 'es2019';
36+
});
37+
await ng('build', '--configuration=production');
38+
const filenameBuild2 = await getScriptsFilename();
39+
await expectFileToMatch(`dist/test-project/${filenameBuild2}`, 'try{console.log()}catch{}');
40+
if (filenameBuild1 === filenameBuild2) {
41+
throw new Error(
42+
'Contents of the built file changed between builds, but the content hash stayed the same!',
43+
);
44+
}
45+
}

0 commit comments

Comments
 (0)