diff --git a/CHANGELOG.md b/CHANGELOG.md
index 35bffb2b98ad..63ef2dbf962c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,38 @@
+
+
+# 19.0.0-next.12 (2024-10-21)
+
+## Breaking Changes
+
+### @angular-devkit/build-angular
+
+- Protractor is no longer supported.
+
+ Protractor was marked end-of-life in August 2023 (see https://protractortest.org/). Projects still relying on Protractor should consider migrating to another E2E testing framework, several support solid migration paths from Protractor.
+
+ - https://angular.dev/tools/cli/end-to-end
+ - https://blog.angular.dev/the-state-of-end-to-end-testing-with-angular-d175f751cb9c
+
+### @angular-devkit/build-angular
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------- |
+| [62877bdf2](https://github.com/angular/angular-cli/commit/62877bdf2b0449d8c12a167c59d0c24c77467f37) | refactor | remove Protractor builder and schematics |
+
+### @angular/build
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- |
+| [1654acf0f](https://github.com/angular/angular-cli/commit/1654acf0ff3010b619a22d11f17eec9975d8e2a2) | fix | relax constraints on external stylesheet component id |
+
+### @angular/ssr
+
+| Commit | Type | Description |
+| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- |
+| [44077f54e](https://github.com/angular/angular-cli/commit/44077f54e9a95afa5c1f85cf198aaa3412ee08d8) | fix | designate package as side-effect free |
+
+
+
# 19.0.0-next.11 (2024-10-16)
diff --git a/WORKSPACE b/WORKSPACE
index 9e97ae77a933..23e3a9d76dbb 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -131,9 +131,9 @@ yarn_install(
http_archive(
name = "aspect_bazel_lib",
- sha256 = "0e31778f1fd574d2c05d238bfc4c785fa4b7e50a5ef38b506e01cfd8ec2fccb3",
- strip_prefix = "bazel-lib-2.9.2",
- url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.9.2/bazel-lib-v2.9.2.tar.gz",
+ sha256 = "a272d79bb0ac6b6965aa199b1f84333413452e87f043b53eca7f347a23a478e8",
+ strip_prefix = "bazel-lib-2.9.3",
+ url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.9.3/bazel-lib-v2.9.3.tar.gz",
)
load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "aspect_bazel_lib_register_toolchains")
diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md
index 1be46d5f16d4..4e2619cee434 100644
--- a/goldens/public-api/angular/build/index.api.md
+++ b/goldens/public-api/angular/build/index.api.md
@@ -48,6 +48,7 @@ export interface ApplicationBuilderOptions {
preserveSymlinks?: boolean;
progress?: boolean;
scripts?: ScriptElement[];
+ security?: Security;
server?: string;
serviceWorker?: ServiceWorker_2;
sourceMap?: SourceMapUnion;
diff --git a/goldens/public-api/angular/ssr/index.api.md b/goldens/public-api/angular/ssr/index.api.md
index 07bd14eb3537..840e3c987b3f 100644
--- a/goldens/public-api/angular/ssr/index.api.md
+++ b/goldens/public-api/angular/ssr/index.api.md
@@ -5,7 +5,6 @@
```ts
import { EnvironmentProviders } from '@angular/core';
-import { InjectionToken } from '@angular/core';
// @public
export class AngularAppEngine {
@@ -35,15 +34,6 @@ export enum RenderMode {
Server = 1
}
-// @public
-export const REQUEST: InjectionToken;
-
-// @public
-export const REQUEST_CONTEXT: InjectionToken;
-
-// @public
-export const RESPONSE_INIT: InjectionToken;
-
// @public
export type ServerRoute = ServerRouteAppShell | ServerRouteClient | ServerRoutePrerender | ServerRoutePrerenderWithParams | ServerRouteServer;
diff --git a/goldens/public-api/angular/ssr/tokens/index.api.md b/goldens/public-api/angular/ssr/tokens/index.api.md
new file mode 100644
index 000000000000..6ac87558b1ad
--- /dev/null
+++ b/goldens/public-api/angular/ssr/tokens/index.api.md
@@ -0,0 +1,20 @@
+## API Report File for "@angular/ssr_tokens"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+import { InjectionToken } from '@angular/core';
+
+// @public
+export const REQUEST: InjectionToken;
+
+// @public
+export const REQUEST_CONTEXT: InjectionToken;
+
+// @public
+export const RESPONSE_INIT: InjectionToken;
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/modules/testing/builder/projects/hello-world-app/angular.json b/modules/testing/builder/projects/hello-world-app/angular.json
index 799af4a7f63e..95607701be8f 100644
--- a/modules/testing/builder/projects/hello-world-app/angular.json
+++ b/modules/testing/builder/projects/hello-world-app/angular.json
@@ -178,7 +178,7 @@
"projectType": "application",
"targets": {
"e2e": {
- "builder": "@angular-devkit/build-angular:protractor",
+ "builder": "@angular-devkit/build-angular:private-protractor",
"options": {
"protractorConfig": "protractor.conf.js",
"devServerTarget": "app:serve",
diff --git a/package.json b/package.json
index d969bee53c4e..51af77b36d3d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@angular/devkit-repo",
- "version": "19.0.0-next.11",
+ "version": "19.0.0-next.12",
"private": true,
"description": "Software Development Kit for Angular",
"keywords": [
@@ -79,7 +79,7 @@
"@babel/plugin-transform-runtime": "7.25.7",
"@babel/preset-env": "7.25.8",
"@babel/runtime": "7.25.7",
- "@bazel/bazelisk": "1.22.0",
+ "@bazel/bazelisk": "1.22.1",
"@bazel/buildifier": "7.3.1",
"@bazel/concatjs": "patch:@bazel/concatjs@npm%3A5.8.1#~/.yarn/patches/@bazel-concatjs-npm-5.8.1-1bf81df846.patch",
"@bazel/jasmine": "patch:@bazel/jasmine@npm%3A5.8.1#~/.yarn/patches/@bazel-jasmine-npm-5.8.1-3370fee155.patch",
@@ -114,8 +114,8 @@
"@types/yargs": "^17.0.20",
"@types/yargs-parser": "^21.0.0",
"@types/yarnpkg__lockfile": "^1.1.5",
- "@typescript-eslint/eslint-plugin": "8.10.0",
- "@typescript-eslint/parser": "8.10.0",
+ "@typescript-eslint/eslint-plugin": "8.11.0",
+ "@typescript-eslint/parser": "8.11.0",
"@vitejs/plugin-basic-ssl": "1.1.0",
"@web/test-runner": "^0.19.0",
"@yarnpkg/lockfile": "1.1.0",
@@ -186,7 +186,7 @@
"rollup": "4.24.0",
"rollup-plugin-sourcemaps": "^0.6.0",
"rxjs": "7.8.1",
- "sass": "1.80.2",
+ "sass": "1.80.3",
"sass-loader": "16.0.2",
"semver": "7.6.3",
"shelljs": "^0.8.5",
diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json
index 01b0f04fb8a3..635fd97e3f89 100644
--- a/packages/angular/build/package.json
+++ b/packages/angular/build/package.json
@@ -39,7 +39,7 @@
"picomatch": "4.0.2",
"piscina": "4.7.0",
"rollup": "4.24.0",
- "sass": "1.80.2",
+ "sass": "1.80.3",
"semver": "7.6.3",
"vite": "5.4.9",
"watchpack": "2.4.2"
diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts
index bc78be6aa3e5..c03d8dc00bb6 100644
--- a/packages/angular/build/src/builders/application/execute-build.ts
+++ b/packages/angular/build/src/builders/application/execute-build.ts
@@ -192,6 +192,11 @@ export async function executeBuild(
);
}
+ // Override auto-CSP settings if we are serving through Vite middleware.
+ if (context.builder.builderName === 'dev-server' && options.security) {
+ options.security.autoCsp = false;
+ }
+
// Perform i18n translation inlining if enabled
if (i18nOptions.shouldInline) {
const result = await inlineI18n(options, executionResult, initialFiles);
diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts
index a454e88e375c..79c0052af18e 100644
--- a/packages/angular/build/src/builders/application/options.ts
+++ b/packages/angular/build/src/builders/application/options.ts
@@ -398,6 +398,7 @@ export async function normalizeOptions(
partialSSRBuild = false,
externalRuntimeStyles,
instrumentForCoverage,
+ security,
} = options;
// Return all the normalized options
@@ -461,6 +462,7 @@ export async function normalizeOptions(
partialSSRBuild: usePartialSsrBuild || partialSSRBuild,
externalRuntimeStyles,
instrumentForCoverage,
+ security,
};
}
diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json
index 2022969a2f10..d47875c6527e 100644
--- a/packages/angular/build/src/builders/application/schema.json
+++ b/packages/angular/build/src/builders/application/schema.json
@@ -37,6 +37,33 @@
"type": "string",
"description": "Customize the base path for the URLs of resources in 'index.html' and component stylesheets. This option is only necessary for specific deployment scenarios, such as with Angular Elements or when utilizing different CDN locations."
},
+ "security": {
+ "description": "Security features to protect against XSS and other common attacks",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "autoCsp": {
+ "description": "Enables automatic generation of a hash-based Strict Content Security Policy (https://web.dev/articles/strict-csp#choose-hash) based on scripts in index.html. Will default to true once we are out of experimental/preview phases.",
+ "default": false,
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "unsafeEval": {
+ "type": "boolean",
+ "description": "Include the `unsafe-eval` directive (https://web.dev/articles/strict-csp#remove-eval) in the auto-CSP. Please only enable this if you are absolutely sure that you need to, as allowing calls to eval will weaken the XSS defenses provided by the auto-CSP.",
+ "default": false
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "boolean"
+ }
+ ]
+ }
+ }
+ },
"scripts": {
"description": "Global scripts to be included in the build.",
"type": "array",
diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts
index 4cb4852e54f7..2e44e0bdab33 100644
--- a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts
+++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts
@@ -76,6 +76,7 @@ export abstract class AngularCompilation {
compilerOptions: ng.CompilerOptions;
referencedFiles: readonly string[];
externalStylesheets?: ReadonlyMap;
+ templateUpdates?: ReadonlyMap;
}>;
abstract emitAffectedFiles(): Iterable | Promise>;
diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
index 9e566803fb58..cbfe70a3e5e5 100644
--- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
+++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
@@ -8,6 +8,7 @@
import type ng from '@angular/compiler-cli';
import assert from 'node:assert';
+import { relative } from 'node:path';
import ts from 'typescript';
import { profileAsync, profileSync } from '../../esbuild/profiling';
import {
@@ -47,6 +48,7 @@ export class AotCompilation extends AngularCompilation {
compilerOptions: ng.CompilerOptions;
referencedFiles: readonly string[];
externalStylesheets?: ReadonlyMap;
+ templateUpdates?: ReadonlyMap;
}> {
// Dynamically load the Angular compiler CLI package
const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli();
@@ -91,6 +93,40 @@ export class AotCompilation extends AngularCompilation {
);
await profileAsync('NG_ANALYZE_PROGRAM', () => angularCompiler.analyzeAsync());
+
+ let templateUpdates;
+ if (
+ compilerOptions['_enableHmr'] &&
+ hostOptions.modifiedFiles &&
+ hasOnlyTemplates(hostOptions.modifiedFiles)
+ ) {
+ const componentNodes = [...hostOptions.modifiedFiles].flatMap((file) => [
+ ...angularCompiler.getComponentsWithTemplateFile(file),
+ ]);
+
+ for (const node of componentNodes) {
+ if (!ts.isClassDeclaration(node)) {
+ continue;
+ }
+ const componentFilename = node.getSourceFile().fileName;
+ let relativePath = relative(host.getCurrentDirectory(), componentFilename);
+ if (relativePath.startsWith('..')) {
+ relativePath = componentFilename;
+ }
+ const updateId = encodeURIComponent(
+ `${host.getCanonicalFileName(relativePath)}@${node.name?.text}`,
+ );
+ const updateText = angularCompiler.emitHmrUpdateModule(node);
+ if (updateText === null) {
+ // Build is needed if a template cannot be updated
+ templateUpdates = undefined;
+ break;
+ }
+ templateUpdates ??= new Map();
+ templateUpdates.set(updateId, updateText);
+ }
+ }
+
const affectedFiles = profileSync('NG_FIND_AFFECTED', () =>
findAffectedFiles(typeScriptProgram, angularCompiler, usingBuildInfo),
);
@@ -131,6 +167,7 @@ export class AotCompilation extends AngularCompilation {
compilerOptions,
referencedFiles,
externalStylesheets: hostOptions.externalStylesheets,
+ templateUpdates,
};
}
@@ -385,3 +422,16 @@ function findAffectedFiles(
return affectedFiles;
}
+
+function hasOnlyTemplates(modifiedFiles: Set): boolean {
+ for (const file of modifiedFiles) {
+ const lowerFile = file.toLowerCase();
+ if (lowerFile.endsWith('.html') || lowerFile.endsWith('.svg')) {
+ continue;
+ }
+
+ return false;
+ }
+
+ return true;
+}
diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts
index a9d1816ac76d..2669951c12e4 100644
--- a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts
+++ b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts
@@ -42,60 +42,62 @@ export async function initialize(request: InitRequest) {
}
});
- const { compilerOptions, referencedFiles, externalStylesheets } = await compilation.initialize(
- request.tsconfig,
- {
- fileReplacements: request.fileReplacements,
- sourceFileCache,
- modifiedFiles: sourceFileCache.modifiedFiles,
- transformStylesheet(data, containingFile, stylesheetFile, order, className) {
- const requestId = randomUUID();
- const resultPromise = new Promise((resolve, reject) =>
- stylesheetRequests.set(requestId, [resolve, reject]),
- );
-
- request.stylesheetPort.postMessage({
- requestId,
- data,
- containingFile,
- stylesheetFile,
- order,
- className,
- });
-
- return resultPromise;
+ const { compilerOptions, referencedFiles, externalStylesheets, templateUpdates } =
+ await compilation.initialize(
+ request.tsconfig,
+ {
+ fileReplacements: request.fileReplacements,
+ sourceFileCache,
+ modifiedFiles: sourceFileCache.modifiedFiles,
+ transformStylesheet(data, containingFile, stylesheetFile, order, className) {
+ const requestId = randomUUID();
+ const resultPromise = new Promise((resolve, reject) =>
+ stylesheetRequests.set(requestId, [resolve, reject]),
+ );
+
+ request.stylesheetPort.postMessage({
+ requestId,
+ data,
+ containingFile,
+ stylesheetFile,
+ order,
+ className,
+ });
+
+ return resultPromise;
+ },
+ processWebWorker(workerFile, containingFile) {
+ Atomics.store(request.webWorkerSignal, 0, 0);
+ request.webWorkerPort.postMessage({ workerFile, containingFile });
+
+ Atomics.wait(request.webWorkerSignal, 0, 0);
+ const result = receiveMessageOnPort(request.webWorkerPort)?.message;
+
+ if (result?.error) {
+ throw result.error;
+ }
+
+ return result?.workerCodeFile ?? workerFile;
+ },
},
- processWebWorker(workerFile, containingFile) {
- Atomics.store(request.webWorkerSignal, 0, 0);
- request.webWorkerPort.postMessage({ workerFile, containingFile });
+ (compilerOptions) => {
+ Atomics.store(request.optionsSignal, 0, 0);
+ request.optionsPort.postMessage(compilerOptions);
- Atomics.wait(request.webWorkerSignal, 0, 0);
- const result = receiveMessageOnPort(request.webWorkerPort)?.message;
+ Atomics.wait(request.optionsSignal, 0, 0);
+ const result = receiveMessageOnPort(request.optionsPort)?.message;
if (result?.error) {
throw result.error;
}
- return result?.workerCodeFile ?? workerFile;
+ return result?.transformedOptions ?? compilerOptions;
},
- },
- (compilerOptions) => {
- Atomics.store(request.optionsSignal, 0, 0);
- request.optionsPort.postMessage(compilerOptions);
-
- Atomics.wait(request.optionsSignal, 0, 0);
- const result = receiveMessageOnPort(request.optionsPort)?.message;
-
- if (result?.error) {
- throw result.error;
- }
-
- return result?.transformedOptions ?? compilerOptions;
- },
- );
+ );
return {
externalStylesheets,
+ templateUpdates,
referencedFiles,
// TODO: Expand? `allowJs`, `isolatedModules`, `sourceMap`, `inlineSourceMap` are the only fields needed currently.
compilerOptions: {
diff --git a/packages/angular/build/src/tools/esbuild/index-html-generator.ts b/packages/angular/build/src/tools/esbuild/index-html-generator.ts
index afe92dfb0b18..4d11ed4fa45a 100644
--- a/packages/angular/build/src/tools/esbuild/index-html-generator.ts
+++ b/packages/angular/build/src/tools/esbuild/index-html-generator.ts
@@ -80,6 +80,15 @@ export async function generateIndexHtml(
throw new Error(`Output file does not exist: ${relativefilePath}`);
};
+ // Read the Auto CSP options.
+ const autoCsp = buildOptions.security?.autoCsp;
+ const autoCspOptions =
+ autoCsp === true
+ ? { unsafeEval: false }
+ : autoCsp
+ ? { unsafeEval: !!autoCsp.unsafeEval }
+ : undefined;
+
// Create an index HTML generator that reads from the in-memory output files
const indexHtmlGenerator = new IndexHtmlGenerator({
indexPath: indexHtmlOptions.input,
@@ -94,6 +103,7 @@ export async function generateIndexHtml(
buildOptions.prerenderOptions ||
buildOptions.appShellOptions
),
+ autoCsp: autoCspOptions,
});
indexHtmlGenerator.readAsset = readAsset;
diff --git a/packages/angular/build/src/tools/esbuild/utils.ts b/packages/angular/build/src/tools/esbuild/utils.ts
index 21ea46ff55d1..3e62c6e7bb1a 100644
--- a/packages/angular/build/src/tools/esbuild/utils.ts
+++ b/packages/angular/build/src/tools/esbuild/utils.ts
@@ -195,7 +195,7 @@ export function getFeatureSupport(
target: string[],
nativeAsyncAwait: boolean,
): BuildOptions['supported'] {
- const supported: Record = {
+ return {
// Native async/await is not supported with Zone.js. Disabling support here will cause
// esbuild to downlevel async/await, async generators, and for await...of to a Zone.js supported form.
'async-await': nativeAsyncAwait,
@@ -205,35 +205,6 @@ export function getFeatureSupport(
// For more details: https://bugs.chromium.org/p/v8/issues/detail?id=11536
'object-rest-spread': false,
};
-
- // Detect Safari browser versions that have a class field behavior bug
- // See: https://github.com/angular/angular-cli/issues/24355#issuecomment-1333477033
- // See: https://github.com/WebKit/WebKit/commit/e8788a34b3d5f5b4edd7ff6450b80936bff396f2
- let safariClassFieldScopeBug = false;
- for (const browser of target) {
- let majorVersion;
- if (browser.startsWith('ios')) {
- majorVersion = Number(browser.slice(3, 5));
- } else if (browser.startsWith('safari')) {
- majorVersion = Number(browser.slice(6, 8));
- } else {
- continue;
- }
- // Technically, 14.0 is not broken but rather does not have support. However, the behavior
- // is identical since it would be set to false by esbuild if present as a target.
- if (majorVersion === 14 || majorVersion === 15) {
- safariClassFieldScopeBug = true;
- break;
- }
- }
- // If class field support cannot be used set to false; otherwise leave undefined to allow
- // esbuild to use `target` to determine support.
- if (safariClassFieldScopeBug) {
- supported['class-field'] = false;
- supported['class-static-field'] = false;
- }
-
- return supported;
}
const MAX_CONCURRENT_WRITES = 64;
diff --git a/packages/angular/build/src/utils/index-file/auto-csp.ts b/packages/angular/build/src/utils/index-file/auto-csp.ts
new file mode 100644
index 000000000000..07e183aaba36
--- /dev/null
+++ b/packages/angular/build/src/utils/index-file/auto-csp.ts
@@ -0,0 +1,303 @@
+/**
+ * @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 * as crypto from 'node:crypto';
+import { StartTag, htmlRewritingStream } from './html-rewriting-stream';
+
+/**
+ * The hash function to use for hash directives to use in the CSP.
+ */
+const HASH_FUNCTION = 'sha256';
+
+/**
+ * Store the appropriate attributes of a sourced script tag to generate the loader script.
+ */
+interface SrcScriptTag {
+ src: string;
+ type?: string;
+ async: boolean;
+ defer: boolean;
+}
+
+/**
+ * Get the specified attribute or return undefined if the tag doesn't have that attribute.
+ *
+ * @param tag StartTag of the `);
+ scriptContent = [];
+ }
+
+ rewriter.on('startTag', (tag, html) => {
+ if (tag.tagName === 'script') {
+ openedScriptTag = tag;
+ const src = getScriptAttributeValue(tag, 'src');
+
+ if (src) {
+ // If there are any interesting attributes, note them down.
+ const scriptType = getScriptAttributeValue(tag, 'type');
+ if (shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) {
+ scriptContent.push({
+ src: src,
+ type: scriptType,
+ async: getScriptAttributeValue(tag, 'async') !== undefined,
+ defer: getScriptAttributeValue(tag, 'defer') !== undefined,
+ });
+
+ return; // Skip writing my script tag until we've read it all.
+ }
+ }
+ }
+ // We are encountering the first start tag that's not tag if it's a part of the
+ // dynamic loader script.
+ if (src && shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) {
+ return;
+ }
+ }
+
+ if (tag.tagName === 'body' || tag.tagName === 'html') {
+ // Write the loader script if a string of
+ Some text
+