Blocked request. This host ("${hostname}") is not allowed.
+To allow this host, add it to allowedHosts under the serve target in angular.json.
{
+ "serve": {
+ "options": {
+ "allowedHosts": ["${hostname}"]
+ }
+ }
+}
+ To allow this host, add it to allowedHosts under the serve target in angular.json.
{
+ "serve": {
+ "options": {
+ "allowedHosts": ["${hostname}"]
+ }
+ }
+}
+ Redirecting to ${url}
-
-
-`.trim();
-}
diff --git a/packages/angular/build/src/utils/server-rendering/render-worker.ts b/packages/angular/build/src/utils/server-rendering/render-worker.ts
index f3fc8e93a0d0..7ded0550b826 100644
--- a/packages/angular/build/src/utils/server-rendering/render-worker.ts
+++ b/packages/angular/build/src/utils/server-rendering/render-worker.ts
@@ -12,6 +12,7 @@ import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loa
import { patchFetchToLoadInMemoryAssets } from './fetch-patch';
import { DEFAULT_URL, launchServer } from './launch-server';
import { loadEsmModuleFromMemory } from './load-esm-from-memory';
+import { generateRedirectStaticPage } from './utils';
export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData {
assetFiles: Record** Destination */ string, /** Source */ string>;
@@ -48,7 +49,13 @@ async function renderPage({ url }: RenderOptions): PromiseRedirecting to ${url}
+
+
+`.trim();
+}
diff --git a/packages/angular/build/src/utils/url.ts b/packages/angular/build/src/utils/url.ts
index d3f1e5791276..689eac37eab5 100644
--- a/packages/angular/build/src/utils/url.ts
+++ b/packages/angular/build/src/utils/url.ts
@@ -6,11 +6,117 @@
* found in the LICENSE file at https://angular.dev/license
*/
-export function urlJoin(...parts: string[]): string {
- const [p, ...rest] = parts;
+/**
+ * Removes the trailing slash from a URL if it exists.
+ *
+ * @param url - The URL string from which to remove the trailing slash.
+ * @returns The URL string without a trailing slash.
+ *
+ * @example
+ * ```js
+ * stripTrailingSlash('path/'); // 'path'
+ * stripTrailingSlash('/path'); // '/path'
+ * stripTrailingSlash('/'); // '/'
+ * stripTrailingSlash(''); // ''
+ * ```
+ */
+export function stripTrailingSlash(url: string): string {
+ // Check if the last character of the URL is a slash
+ return url.length > 1 && url.at(-1) === '/' ? url.slice(0, -1) : url;
+}
+
+/**
+ * Removes the leading slash from a URL if it exists.
+ *
+ * @param url - The URL string from which to remove the leading slash.
+ * @returns The URL string without a leading slash.
+ *
+ * @example
+ * ```js
+ * stripLeadingSlash('/path'); // 'path'
+ * stripLeadingSlash('/path/'); // 'path/'
+ * stripLeadingSlash('/'); // '/'
+ * stripLeadingSlash(''); // ''
+ * ```
+ */
+export function stripLeadingSlash(url: string): string {
+ // Check if the first character of the URL is a slash
+ return url.length > 1 && url[0] === '/' ? url.slice(1) : url;
+}
+
+/**
+ * Adds a leading slash to a URL if it does not already have one.
+ *
+ * @param url - The URL string to which the leading slash will be added.
+ * @returns The URL string with a leading slash.
+ *
+ * @example
+ * ```js
+ * addLeadingSlash('path'); // '/path'
+ * addLeadingSlash('/path'); // '/path'
+ * ```
+ */
+export function addLeadingSlash(url: string): string {
+ // Check if the URL already starts with a slash
+ return url[0] === '/' ? url : `/${url}`;
+}
+
+/**
+ * Adds a trailing slash to a URL if it does not already have one.
+ *
+ * @param url - The URL string to which the trailing slash will be added.
+ * @returns The URL string with a trailing slash.
+ *
+ * @example
+ * ```js
+ * addTrailingSlash('path'); // 'path/'
+ * addTrailingSlash('path/'); // 'path/'
+ * ```
+ */
+export function addTrailingSlash(url: string): string {
+ // Check if the URL already end with a slash
+ return url.at(-1) === '/' ? url : `${url}/`;
+}
+
+/**
+ * Joins URL parts into a single URL string.
+ *
+ * This function takes multiple URL segments, normalizes them by removing leading
+ * and trailing slashes where appropriate, and then joins them into a single URL.
+ *
+ * @param parts - The parts of the URL to join. Each part can be a string with or without slashes.
+ * @returns The joined URL string, with normalized slashes.
+ *
+ * @example
+ * ```js
+ * joinUrlParts('path/', '/to/resource'); // '/path/to/resource'
+ * joinUrlParts('/path/', 'to/resource'); // '/path/to/resource'
+ * joinUrlParts('http://localhost/path/', 'to/resource'); // 'http://localhost/path/to/resource'
+ * joinUrlParts('', ''); // '/'
+ * ```
+ */
+export function joinUrlParts(...parts: string[]): string {
+ const normalizeParts: string[] = [];
+ for (const part of parts) {
+ if (part === '') {
+ // Skip any empty parts
+ continue;
+ }
+
+ let normalizedPart = part;
+ if (part[0] === '/') {
+ normalizedPart = normalizedPart.slice(1);
+ }
+ if (part.at(-1) === '/') {
+ normalizedPart = normalizedPart.slice(0, -1);
+ }
+ if (normalizedPart !== '') {
+ normalizeParts.push(normalizedPart);
+ }
+ }
+
+ const protocolMatch = normalizeParts.length && /^https?:\/\//.test(normalizeParts[0]);
+ const joinedParts = normalizeParts.join('/');
- // Remove trailing slash from first part
- // Join all parts with `/`
- // Dedupe double slashes from path names
- return p.replace(/\/$/, '') + ('/' + rest.join('/')).replace(/\/\/+/g, '/');
+ return protocolMatch ? joinedParts : addLeadingSlash(joinedParts);
}
diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel
index 6cbb09b36c31..abed616cd810 100644
--- a/packages/angular/cli/BUILD.bazel
+++ b/packages/angular/cli/BUILD.bazel
@@ -4,8 +4,7 @@
# found in the LICENSE file at https://angular.dev/license
load("@npm//:defs.bzl", "npm_link_all_packages")
-load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project")
-load("//tools:example_db_generator.bzl", "cli_example_db")
+load("//tools:defaults.bzl", "jasmine_test", "ng_examples_db", "npm_package", "ts_project")
load("//tools:ng_cli_schema_generator.bzl", "cli_json_schema")
load("//tools:ts_json_schema.bzl", "ts_json_schema")
@@ -15,6 +14,17 @@ package(default_visibility = ["//visibility:public"])
npm_link_all_packages()
+genrule(
+ name = "angular_best_practices",
+ srcs = [
+ "//:node_modules/@angular/core/dir",
+ ],
+ outs = ["src/commands/mcp/resources/best-practices.md"],
+ cmd = """
+ cp "$(location //:node_modules/@angular/core/dir)/resources/best-practices.md" $@
+ """,
+)
+
RUNTIME_ASSETS = glob(
include = [
"bin/**/*",
@@ -27,6 +37,7 @@ RUNTIME_ASSETS = glob(
) + [
"//packages/angular/cli:lib/config/schema.json",
"//packages/angular/cli:lib/code-examples.db",
+ ":angular_best_practices",
]
ts_project(
@@ -38,6 +49,7 @@ ts_project(
],
exclude = [
"**/*_spec.ts",
+ "**/testing/**",
],
) + [
# These files are generated from the JSON schema
@@ -56,6 +68,7 @@ ts_project(
":node_modules/algoliasearch",
":node_modules/ini",
":node_modules/jsonc-parser",
+ ":node_modules/listr2",
":node_modules/npm-package-arg",
":node_modules/pacote",
":node_modules/parse5-html-rewriting-stream",
@@ -71,13 +84,12 @@ ts_project(
"//:node_modules/@types/semver",
"//:node_modules/@types/yargs",
"//:node_modules/@types/yarnpkg__lockfile",
- "//:node_modules/listr2",
"//:node_modules/semver",
"//:node_modules/typescript",
],
)
-cli_example_db(
+ng_examples_db(
name = "cli_example_database",
srcs = glob(
include = [
@@ -116,7 +128,10 @@ ts_project(
name = "angular-cli_test_lib",
testonly = True,
srcs = glob(
- include = ["**/*_spec.ts"],
+ include = [
+ "**/*_spec.ts",
+ "**/testing/**",
+ ],
exclude = [
# NB: we need to exclude the nested node_modules that is laid out by yarn workspaces
"node_modules/**",
diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json
index 06034d7c5f78..786815f3c982 100644
--- a/packages/angular/cli/package.json
+++ b/packages/angular/cli/package.json
@@ -25,22 +25,22 @@
"@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER",
"@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER",
"@angular-devkit/schematics": "workspace:0.0.0-PLACEHOLDER",
- "@inquirer/prompts": "7.9.0",
+ "@inquirer/prompts": "7.10.1",
"@listr2/prompt-adapter-inquirer": "3.0.5",
- "@modelcontextprotocol/sdk": "1.20.1",
+ "@modelcontextprotocol/sdk": "1.24.3",
"@schematics/angular": "workspace:0.0.0-PLACEHOLDER",
"@yarnpkg/lockfile": "1.1.0",
- "algoliasearch": "5.40.1",
- "ini": "5.0.0",
+ "algoliasearch": "5.46.0",
+ "ini": "6.0.0",
"jsonc-parser": "3.3.1",
"listr2": "9.0.5",
- "npm-package-arg": "13.0.1",
- "pacote": "21.0.3",
+ "npm-package-arg": "13.0.2",
+ "pacote": "21.0.4",
"parse5-html-rewriting-stream": "8.0.0",
"resolve": "1.22.11",
"semver": "7.7.3",
"yargs": "18.0.0",
- "zod": "3.25.76"
+ "zod": "4.1.13"
},
"ng-update": {
"migrations": "@schematics/angular/migrations/migration-collection.json",
diff --git a/packages/angular/cli/src/command-builder/architect-base-command-module.ts b/packages/angular/cli/src/command-builder/architect-base-command-module.ts
index 566e0e62b209..fb3508777d74 100644
--- a/packages/angular/cli/src/command-builder/architect-base-command-module.ts
+++ b/packages/angular/cli/src/command-builder/architect-base-command-module.ts
@@ -12,8 +12,7 @@ import {
WorkspaceNodeModulesArchitectHost,
} from '@angular-devkit/architect/node';
import { json } from '@angular-devkit/core';
-import { existsSync } from 'node:fs';
-import { resolve } from 'node:path';
+import { createRequire } from 'node:module';
import { isPackageNameSafeForAnalytics } from '../analytics/analytics';
import { EventCustomDimension, EventCustomMetric } from '../analytics/analytics-parameters';
import { assertIsError } from '../utilities/error';
@@ -210,15 +209,13 @@ export abstract class ArchitectBaseCommandModule