diff --git a/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts b/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts
index f3ec9476b21f..5153045dba73 100644
--- a/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts
+++ b/packages/angular/build/src/builders/application/tests/options/subresource-integrity_spec.ts
@@ -6,8 +6,25 @@
* found in the LICENSE file at https://angular.dev/license
*/
+import { getSystemPath } from '@angular-devkit/core';
+import { createHash } from 'node:crypto';
+import { readdirSync, readFileSync } from 'node:fs';
+import { join } from 'node:path';
import { buildApplication } from '../../index';
-import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder, expectNoLog } from '../setup';
+import {
+ APPLICATION_BUILDER_INFO,
+ BASE_OPTIONS,
+ describeBuilder,
+ expectNoLog,
+ host,
+ lazyModuleFiles,
+ lazyModuleFnImport,
+} from '../setup';
+
+/** Resolve a path inside the harness workspace synchronously. */
+function workspacePath(...segments: string[]): string {
+ return join(getSystemPath(host.root()), ...segments);
+}
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
describe('Option: "subresourceIntegrity"', () => {
@@ -65,5 +82,84 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
.content.toMatch(/integrity="\w+-[A-Za-z0-9/+=]+"/);
expectNoLog(logs, /subresource-integrity/);
});
+
+ it(`emits an importmap with integrity for lazy chunks when 'true'`, async () => {
+ await harness.writeFiles(lazyModuleFiles);
+ await harness.writeFiles(lazyModuleFnImport);
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ subresourceIntegrity: true,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+
+ const indexHtml = harness.readFile('dist/browser/index.html');
+ const match = indexHtml.match(/`);
}
+ let subResourceIntegrityTag: string | undefined;
let headerLinkTags: string[] = [];
let bodyLinkTags: string[] = [];
+
+ // Emit an integrity-only import map so the browser can validate lazy chunks
+ // resolved via dynamic `import()` (which otherwise carry no SRI metadata).
+ // The block is placed first inside `
` so it precedes any module
+ // script, as required by the import-map spec.
+ if (sri && chunksIntegrity?.size) {
+ const integrity: Record = {};
+ // Stable iteration order for reproducible builds.
+ const sortedEntries = [...chunksIntegrity.entries()].sort(([keyA], [keyB]) =>
+ keyA.localeCompare(keyB),
+ );
+ for (const [url, integrityHash] of sortedEntries) {
+ integrity[generateUrl(url, deployUrl)] = integrityHash;
+ }
+ const importMapJson = JSON.stringify({ integrity }).replace(/${importMapJson}`;
+ }
+
for (const src of stylesheets) {
const attrs = [`rel="stylesheet"`, `href="${generateUrl(src, deployUrl)}"`];
@@ -212,6 +256,9 @@ export async function augmentIndexHtml(
if (!baseTagExists && isString(baseHref)) {
rewriter.emitStartTag(tag);
rewriter.emitRaw(``);
+ if (subResourceIntegrityTag) {
+ rewriter.emitRaw(subResourceIntegrityTag);
+ }
return;
}
@@ -221,6 +268,9 @@ export async function augmentIndexHtml(
if (isString(baseHref)) {
updateAttribute(tag, 'href', baseHref);
}
+ if (subResourceIntegrityTag) {
+ rewriter.emitRaw(subResourceIntegrityTag);
+ }
break;
case 'link':
if (readAttribute(tag, 'rel') === 'preconnect') {
diff --git a/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts b/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts
index 55adf8d88f0b..d9a5d1a753d4 100644
--- a/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts
+++ b/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts
@@ -459,6 +459,22 @@ describe('augment-index-html', () => {
);
});
+ it('should escape `<` characters in inline importmap JSON', async () => {
+ const { content } = await augmentIndexHtml({
+ ...indexGeneratorOptions,
+ sri: true,
+ chunksIntegrity: new Map([['lazy([^<]+)<\/script>/);
+ expect(match).withContext('importmap script tag missing').not.toBeNull();
+ expect(match![1]).toContain('lazy\\u003cchunk.js');
+ expect(match![1]).not.toContain('lazy {
const imageDomains = ['https://www.example.com', 'https://www.example2.com'];
const { content, warnings } = await augmentIndexHtml({
diff --git a/packages/angular/build/src/utils/index-file/index-html-generator.ts b/packages/angular/build/src/utils/index-file/index-html-generator.ts
index 52a926ef58eb..fcece48647ae 100644
--- a/packages/angular/build/src/utils/index-file/index-html-generator.ts
+++ b/packages/angular/build/src/utils/index-file/index-html-generator.ts
@@ -49,6 +49,13 @@ export interface IndexHtmlGeneratorOptions {
imageDomains?: string[];
generateDedicatedSSRContent?: boolean;
autoCsp?: AutoCspOptions;
+
+ /**
+ * Integrity metadata for module URLs not directly referenced in the index
+ * (typically lazy-loaded chunks). Forwarded to {@link augmentIndexHtml} so
+ * a `