diff --git a/.idea/copilotDiffState.xml b/.idea/copilotDiffState.xml
new file mode 100644
index 000000000000..4dbcb4efe03e
--- /dev/null
+++ b/.idea/copilotDiffState.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/angular/build/src/builders/application/execute-post-bundle.ts b/packages/angular/build/src/builders/application/execute-post-bundle.ts
index 5171ca254d5d..d7fcb15b8748 100644
--- a/packages/angular/build/src/builders/application/execute-post-bundle.ts
+++ b/packages/angular/build/src/builders/application/execute-post-bundle.ts
@@ -30,6 +30,7 @@ import {
} from '../../utils/server-rendering/models';
import { prerenderPages } from '../../utils/server-rendering/prerender';
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
+import { injectDebugIds } from './inject-debug-ids';
import { INDEX_HTML_CSR, INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options';
import { OutputMode } from './schema';
@@ -79,6 +80,14 @@ export async function executePostBundleSteps(
partialSSRBuild,
} = options;
+ // Embed ECMA-426 Debug IDs into JS/source-map pairs before any consumer reads the bytes (in
+ // particular `generateIndexHtml` below, which computes SRI hashes from the on-disk content).
+ // Doing this here also covers the i18n path, where this function is invoked once per locale
+ // with locale-specific output files. Files without a source map sibling are skipped.
+ if (sourcemapOptions.scripts) {
+ injectDebugIds(outputFiles);
+ }
+
// Index HTML content without CSS inlining to be used for server rendering (AppShell, SSG and SSR).
// NOTE: Critical CSS inlining is deliberately omitted here, as it will be handled during server rendering.
// Additionally, when using prerendering or AppShell, the index HTML file may be regenerated.
diff --git a/packages/angular/build/src/builders/application/inject-debug-ids.ts b/packages/angular/build/src/builders/application/inject-debug-ids.ts
new file mode 100644
index 000000000000..8c6cf624c29b
--- /dev/null
+++ b/packages/angular/build/src/builders/application/inject-debug-ids.ts
@@ -0,0 +1,49 @@
+/**
+ * @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 { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
+import {
+ generateDebugId,
+ injectDebugIdIntoJs,
+ injectDebugIdIntoSourceMap,
+} from '../../utils/debug-id';
+
+/**
+ * Embeds an ECMA-426 Debug ID into every browser JavaScript output that has a
+ * matching source map sibling.
+ *
+ * The Debug ID is derived deterministically (UUIDv5) from the source map bytes
+ * so rebuilds of the same source produce the same ID. The JS file gets a
+ * `//# debugId=` comment placed above any existing
+ * `//# sourceMappingURL=` line and the source map JSON gets a top-level
+ * `"debugId"` field. Together they make build artifacts self-identifying as
+ * proposed by https://github.com/tc39/ecma426/blob/main/proposals/debug-id.md.
+ */
+export function injectDebugIds(outputFiles: BuildOutputFile[]): void {
+ const filesByPath = new Map();
+ for (const file of outputFiles) {
+ filesByPath.set(file.path, file);
+ }
+
+ const encoder = new TextEncoder();
+
+ for (const file of outputFiles) {
+ if (file.type !== BuildOutputFileType.Browser || !file.path.endsWith('.js')) {
+ continue;
+ }
+
+ const map = filesByPath.get(`${file.path}.map`);
+ if (!map) {
+ continue;
+ }
+
+ const id = generateDebugId(map.contents);
+ file.contents = encoder.encode(injectDebugIdIntoJs(file.text, id));
+ map.contents = encoder.encode(injectDebugIdIntoSourceMap(map.text, id));
+ }
+}
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..a369f5561385 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,23 @@
* 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,
+} 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 +80,82 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
.content.toMatch(/integrity="\w+-[A-Za-z0-9/+=]+"/);
expectNoLog(logs, /subresource-integrity/);
});
+
+ it(`embeds an ECMA-426 debugId in JS and source map and the integrity matches`, async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ subresourceIntegrity: true,
+ sourceMap: { scripts: true },
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+
+ const distDir = workspacePath('dist/browser');
+ const allEntries = readdirSync(distDir);
+ const jsFiles = allEntries.filter(
+ (f) => f.endsWith('.js') && allEntries.includes(`${f}.map`),
+ );
+ expect(jsFiles.length).toBeGreaterThan(0);
+
+ const debugIdRe = /^\s*\/\/\s*#\s*debugId=([0-9a-f-]+)\s*$/m;
+ const indexHtml = harness.readFile('dist/browser/index.html');
+ const importmapMatch = indexHtml.match(/