From a8f463227492ae49d6c3f708851e3c79e0da953a Mon Sep 17 00:00:00 2001 From: drakeo338 Date: Mon, 19 Oct 2026 09:39:44 +0000 Subject: [PATCH 1/2] fix(@angular/build): add integrity metadata for all initial chunks beyond the module preload limit With subresource integrity enabled, the import map integrity entries skipped every file in the initial files set. Only the first 10 initial script chunks receive a `modulepreload` hint (and none do when `preloadInitial` is disabled or `externalPackages` is set), so the remaining initial chunks ended up without any integrity metadata. Skip only entry points, which are referenced by their own tag, and files that actually received a hint. All other initial chunks are now included in the import map integrity entries. Closes #34395 --- .../application/index-html-generator.ts | 9 +- .../application/index-html-generator_spec.ts | 114 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 packages/angular/build/src/builders/application/index-html-generator_spec.ts diff --git a/packages/angular/build/src/builders/application/index-html-generator.ts b/packages/angular/build/src/builders/application/index-html-generator.ts index d89eadf8b9b0..a1c9cbcc9b30 100644 --- a/packages/angular/build/src/builders/application/index-html-generator.ts +++ b/packages/angular/build/src/builders/application/index-html-generator.ts @@ -91,8 +91,15 @@ export async function generateIndexHtml( let chunksIntegrity: ReadonlyMap | undefined; if (subresourceIntegrity) { const integrity = new Map(); + // Entry points and module preload hints carry their own integrity attribute. Any other + // initial chunk (e.g. beyond the module preload limit) is only covered by the import map. + const hinted = new Set(hints.map(({ url }) => url)); for (const file of browserOutputFiles) { - if (!file.path.endsWith('.js') || initialFiles.has(file.path)) { + if ( + !file.path.endsWith('.js') || + initialFiles.get(file.path)?.entrypoint || + hinted.has(file.path) + ) { continue; } const hash = createHash('sha384').update(file.contents).digest('base64'); diff --git a/packages/angular/build/src/builders/application/index-html-generator_spec.ts b/packages/angular/build/src/builders/application/index-html-generator_spec.ts new file mode 100644 index 000000000000..4f1098086461 --- /dev/null +++ b/packages/angular/build/src/builders/application/index-html-generator_spec.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + BuildOutputFile, + BuildOutputFileType, + InitialFileRecord, +} from '../../tools/esbuild/bundler-files'; +import { generateIndexHtml } from './index-html-generator'; +import { NormalizedApplicationBuildOptions } from './options'; + +describe('generateIndexHtml', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), 'index-html-generator-spec-')); + await writeFile( + join(tempDir, 'index.html'), + '', + ); + }); + + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + function createOutputFile(path: string): BuildOutputFile { + const text = `console.log(${JSON.stringify(path)});`; + + return { + path, + text, + contents: Buffer.from(text), + type: BuildOutputFileType.Browser, + } as unknown as BuildOutputFile; + } + + function createOptions(subresourceIntegrity: boolean): NormalizedApplicationBuildOptions { + return { + indexHtmlOptions: { + input: join(tempDir, 'index.html'), + insertionOrder: [['main.js', true]], + preloadInitial: true, + }, + subresourceIntegrity, + baseHref: '/', + security: {}, + } as unknown as NormalizedApplicationBuildOptions; + } + + it('adds integrity metadata for every initial chunk when there are more than the module preload limit', async () => { + const chunkCount = 15; + const chunks = Array.from({ length: chunkCount }, (_, i) => `chunk-${i}.js`); + const initialFiles = new Map([ + ['main.js', { entrypoint: true, name: 'main', type: 'script', serverFile: false, depth: 0 }], + ...chunks.map( + (chunk, i) => + [chunk, { entrypoint: false, type: 'script', serverFile: false, depth: i + 1 }] as [ + string, + InitialFileRecord, + ], + ), + ]); + const outputFiles = ['main.js', ...chunks].map(createOutputFile); + + const { csrContent, errors } = await generateIndexHtml( + initialFiles, + outputFiles, + createOptions(true), + ); + expect(errors).toEqual([]); + + const importMapMatch = /