@angular/build 21.0.0-next.4 → 21.0.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/build",
3
- "version": "21.0.0-next.4",
3
+ "version": "21.0.0-next.5",
4
4
  "description": "Official build system for Angular",
5
5
  "keywords": [
6
6
  "Angular CLI",
@@ -23,7 +23,7 @@
23
23
  "builders": "builders.json",
24
24
  "dependencies": {
25
25
  "@ampproject/remapping": "2.3.0",
26
- "@angular-devkit/architect": "0.2100.0-next.4",
26
+ "@angular-devkit/architect": "0.2100.0-next.5",
27
27
  "@babel/core": "7.28.4",
28
28
  "@babel/helper-annotate-as-pure": "7.27.3",
29
29
  "@babel/helper-split-export-declaration": "7.24.7",
@@ -31,7 +31,7 @@
31
31
  "@vitejs/plugin-basic-ssl": "2.1.0",
32
32
  "beasties": "0.3.5",
33
33
  "browserslist": "^4.26.0",
34
- "esbuild": "0.25.9",
34
+ "esbuild": "0.25.10",
35
35
  "https-proxy-agent": "7.0.6",
36
36
  "istanbul-lib-instrument": "6.0.3",
37
37
  "jsonc-parser": "3.3.1",
@@ -41,12 +41,12 @@
41
41
  "parse5-html-rewriting-stream": "8.0.0",
42
42
  "picomatch": "4.0.3",
43
43
  "piscina": "5.1.3",
44
- "rolldown": "1.0.0-beta.38",
45
- "sass": "1.92.1",
44
+ "rolldown": "1.0.0-beta.39",
45
+ "sass": "1.93.1",
46
46
  "semver": "7.7.2",
47
47
  "source-map-support": "0.5.21",
48
48
  "tinyglobby": "0.2.15",
49
- "vite": "7.1.5",
49
+ "vite": "7.1.7",
50
50
  "watchpack": "2.4.4"
51
51
  },
52
52
  "optionalDependencies": {
@@ -60,7 +60,7 @@
60
60
  "@angular/platform-browser": "^21.0.0-next.0",
61
61
  "@angular/platform-server": "^21.0.0-next.0",
62
62
  "@angular/service-worker": "^21.0.0-next.0",
63
- "@angular/ssr": "^21.0.0-next.4",
63
+ "@angular/ssr": "^21.0.0-next.5",
64
64
  "karma": "^6.4.0",
65
65
  "less": "^4.2.0",
66
66
  "ng-packagr": "^21.0.0-next.0",
@@ -112,7 +112,7 @@
112
112
  "type": "git",
113
113
  "url": "https://github.com/angular/angular-cli.git"
114
114
  },
115
- "packageManager": "pnpm@10.17.0",
115
+ "packageManager": "pnpm@10.17.1",
116
116
  "engines": {
117
117
  "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
118
118
  "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
@@ -155,14 +155,18 @@ async function setupBuildOptions(options, context, projectSourceRoot, outputPath
155
155
  return { buildOptions, mainName };
156
156
  }
157
157
  async function runEsbuild(buildOptions, context, projectSourceRoot) {
158
+ const usesZoneJS = buildOptions.polyfills?.includes('zone.js');
158
159
  const virtualTestBedInit = (0, virtual_module_plugin_1.createVirtualModulePlugin)({
159
160
  namespace: 'angular:test-bed-init',
160
161
  loadContent: async () => {
161
162
  const contents = [
162
163
  // Initialize the Angular testing environment
164
+ `import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core';`,
163
165
  `import { getTestBed } from '@angular/core/testing';`,
164
166
  `import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`,
165
- `getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {`,
167
+ `@NgModule({ providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}], })`,
168
+ `export class TestModule {}`,
169
+ `getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {`,
166
170
  ` errorOnUnknownElements: true,`,
167
171
  ` errorOnUnknownProperties: true,`,
168
172
  `});`,
@@ -17,7 +17,7 @@ const tinyglobby_1 = require("tinyglobby");
17
17
  function createInstrumentationFilter(includedBasePath, excludedPaths) {
18
18
  return (request) => {
19
19
  return (!excludedPaths.has(request) &&
20
- !/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]/.test(request) &&
20
+ !/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]|[\\/]\.angular[\\/]/.test(request) &&
21
21
  request.startsWith(includedBasePath));
22
22
  };
23
23
  }
@@ -5,12 +5,4 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
- export declare function findTests(include: string[], exclude: string[], workspaceRoot: string, projectSourceRoot: string): Promise<string[]>;
9
- interface TestEntrypointsOptions {
10
- projectSourceRoot: string;
11
- workspaceRoot: string;
12
- removeTestExtension?: boolean;
13
- }
14
- /** Generate unique bundle names for a set of test files. */
15
- export declare function getTestEntrypoints(testFiles: string[], { projectSourceRoot, workspaceRoot, removeTestExtension }: TestEntrypointsOptions): Map<string, string>;
16
- export {};
8
+ export { findTests, getTestEntrypoints } from '../unit-test/test-discovery';
@@ -7,109 +7,9 @@
7
7
  * found in the LICENSE file at https://angular.dev/license
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.findTests = findTests;
11
- exports.getTestEntrypoints = getTestEntrypoints;
12
- const node_fs_1 = require("node:fs");
13
- const node_path_1 = require("node:path");
14
- const tinyglobby_1 = require("tinyglobby");
15
- const path_1 = require("../../utils/path");
16
- /* Go through all patterns and find unique list of files */
17
- async function findTests(include, exclude, workspaceRoot, projectSourceRoot) {
18
- const matchingTestsPromises = include.map((pattern) => findMatchingTests(pattern, exclude, workspaceRoot, projectSourceRoot));
19
- const files = await Promise.all(matchingTestsPromises);
20
- // Unique file names
21
- return [...new Set(files.flat())];
22
- }
23
- /** Generate unique bundle names for a set of test files. */
24
- function getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot, removeTestExtension }) {
25
- const seen = new Set();
26
- return new Map(Array.from(testFiles, (testFile) => {
27
- const relativePath = removeRoots(testFile, [projectSourceRoot, workspaceRoot])
28
- // Strip leading dots and path separators.
29
- .replace(/^[./\\]+/, '')
30
- // Replace any path separators with dashes.
31
- .replace(/[/\\]/g, '-');
32
- let fileName = (0, node_path_1.basename)(relativePath, (0, node_path_1.extname)(relativePath));
33
- if (removeTestExtension) {
34
- fileName = fileName.replace(/\.(spec|test)$/, '');
35
- }
36
- const baseName = `spec-${fileName}`;
37
- let uniqueName = baseName;
38
- let suffix = 2;
39
- while (seen.has(uniqueName)) {
40
- uniqueName = `${baseName}-${suffix}`.replace(/([^\w](?:spec|test))-([\d]+)$/, '-$2$1');
41
- ++suffix;
42
- }
43
- seen.add(uniqueName);
44
- return [uniqueName, testFile];
45
- }));
46
- }
47
- const removeLeadingSlash = (pattern) => {
48
- if (pattern.charAt(0) === '/') {
49
- return pattern.substring(1);
50
- }
51
- return pattern;
52
- };
53
- const removeRelativeRoot = (path, root) => {
54
- if (path.startsWith(root)) {
55
- return path.substring(root.length);
56
- }
57
- return path;
58
- };
59
- function removeRoots(path, roots) {
60
- for (const root of roots) {
61
- if (path.startsWith(root)) {
62
- return path.substring(root.length);
63
- }
64
- }
65
- return (0, node_path_1.basename)(path);
66
- }
67
- async function findMatchingTests(pattern, ignore, workspaceRoot, projectSourceRoot) {
68
- // normalize pattern, glob lib only accepts forward slashes
69
- let normalizedPattern = (0, path_1.toPosixPath)(pattern);
70
- normalizedPattern = removeLeadingSlash(normalizedPattern);
71
- const relativeProjectRoot = (0, path_1.toPosixPath)((0, node_path_1.relative)(workspaceRoot, projectSourceRoot) + '/');
72
- // remove relativeProjectRoot to support relative paths from root
73
- // such paths are easy to get when running scripts via IDEs
74
- normalizedPattern = removeRelativeRoot(normalizedPattern, relativeProjectRoot);
75
- // special logic when pattern does not look like a glob
76
- if (!(0, tinyglobby_1.isDynamicPattern)(normalizedPattern)) {
77
- if (await isDirectory((0, node_path_1.join)(projectSourceRoot, normalizedPattern))) {
78
- normalizedPattern = `${normalizedPattern}/**/*.spec.@(ts|tsx)`;
79
- }
80
- else {
81
- // see if matching spec file exists
82
- const fileExt = (0, node_path_1.extname)(normalizedPattern);
83
- // Replace extension to `.spec.ext`. Example: `src/app/app.component.ts`-> `src/app/app.component.spec.ts`
84
- const potentialSpec = (0, node_path_1.join)(projectSourceRoot, (0, node_path_1.dirname)(normalizedPattern), `${(0, node_path_1.basename)(normalizedPattern, fileExt)}.spec${fileExt}`);
85
- if (await exists(potentialSpec)) {
86
- return [potentialSpec];
87
- }
88
- }
89
- }
90
- // normalize the patterns in the ignore list
91
- const normalizedIgnorePatternList = ignore.map((pattern) => removeRelativeRoot(removeLeadingSlash((0, path_1.toPosixPath)(pattern)), relativeProjectRoot));
92
- return (0, tinyglobby_1.glob)(normalizedPattern, {
93
- cwd: projectSourceRoot,
94
- absolute: true,
95
- ignore: ['**/node_modules/**', ...normalizedIgnorePatternList],
96
- });
97
- }
98
- async function isDirectory(path) {
99
- try {
100
- const stats = await node_fs_1.promises.stat(path);
101
- return stats.isDirectory();
102
- }
103
- catch {
104
- return false;
105
- }
106
- }
107
- async function exists(path) {
108
- try {
109
- await node_fs_1.promises.access(path, node_fs_1.constants.F_OK);
110
- return true;
111
- }
112
- catch {
113
- return false;
114
- }
115
- }
10
+ exports.getTestEntrypoints = exports.findTests = void 0;
11
+ // This file is a compatibility layer that re-exports the test discovery logic from its new location.
12
+ // This is necessary to avoid breaking the Karma builder, which still depends on this file.
13
+ var test_discovery_1 = require("../unit-test/test-discovery");
14
+ Object.defineProperty(exports, "findTests", { enumerable: true, get: function () { return test_discovery_1.findTests; } });
15
+ Object.defineProperty(exports, "getTestEntrypoints", { enumerable: true, get: function () { return test_discovery_1.getTestEntrypoints; } });
@@ -107,6 +107,7 @@ const application_1 = require("../application");
107
107
  const results_1 = require("../application/results");
108
108
  const options_1 = require("./options");
109
109
  const dependency_checker_1 = require("./runners/dependency-checker");
110
+ const test_discovery_1 = require("./test-discovery");
110
111
  async function loadTestRunner(runnerName) {
111
112
  // Harden against directory traversal
112
113
  if (!/^[a-zA-Z0-9-]+$/.test(runnerName)) {
@@ -226,6 +227,15 @@ async function* execute(options, context, extensions) {
226
227
  yield { success: false };
227
228
  return;
228
229
  }
230
+ if (normalizedOptions.listTests) {
231
+ const testFiles = await (0, test_discovery_1.findTests)(normalizedOptions.include, normalizedOptions.exclude ?? [], normalizedOptions.workspaceRoot, normalizedOptions.projectSourceRoot);
232
+ context.logger.info('Discovered test files:');
233
+ for (const file of testFiles) {
234
+ context.logger.info(` ${node_path_1.default.relative(normalizedOptions.workspaceRoot, file)}`);
235
+ }
236
+ yield { success: true };
237
+ return;
238
+ }
229
239
  if (runner.isStandalone) {
230
240
  try {
231
241
  const env_1 = { stack: [], error: void 0, hasError: false };
@@ -32,5 +32,6 @@ export declare function normalizeOptions(context: BuilderContext, projectName: s
32
32
  providersFile: string | undefined;
33
33
  setupFiles: string[];
34
34
  dumpVirtualFiles: boolean | undefined;
35
+ listTests: boolean | undefined;
35
36
  }>;
36
37
  export declare function injectTestingPolyfills(polyfills?: string[]): string[];
@@ -64,6 +64,7 @@ async function normalizeOptions(context, projectName, options) {
64
64
  ? options.setupFiles.map((setupFile) => node_path_1.default.join(workspaceRoot, setupFile))
65
65
  : [],
66
66
  dumpVirtualFiles: options.dumpVirtualFiles,
67
+ listTests: options.listTests,
67
68
  };
68
69
  }
69
70
  function injectTestingPolyfills(polyfills = []) {
@@ -16,7 +16,8 @@ const path_1 = require("../../../../utils/path");
16
16
  const schema_1 = require("../../../application/schema");
17
17
  const options_1 = require("../../options");
18
18
  const test_discovery_1 = require("../../test-discovery");
19
- function createTestBedInitVirtualFile(providersFile, projectSourceRoot) {
19
+ function createTestBedInitVirtualFile(providersFile, projectSourceRoot, polyfills = []) {
20
+ const usesZoneJS = polyfills.includes('zone.js');
20
21
  let providersImport = 'const providers = [];';
21
22
  if (providersFile) {
22
23
  const relativePath = node_path_1.default.relative(projectSourceRoot, providersFile);
@@ -26,7 +27,7 @@ function createTestBedInitVirtualFile(providersFile, projectSourceRoot) {
26
27
  }
27
28
  return `
28
29
  // Initialize the Angular testing environment
29
- import { NgModule } from '@angular/core';
30
+ import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core';
30
31
  import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing';
31
32
  import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
32
33
  ${providersImport}
@@ -34,7 +35,7 @@ function createTestBedInitVirtualFile(providersFile, projectSourceRoot) {
34
35
  beforeEach(getCleanupHook(false));
35
36
  afterEach(getCleanupHook(true));
36
37
  @NgModule({
37
- providers,
38
+ providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}...providers],
38
39
  })
39
40
  export class TestModule {}
40
41
  getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {
@@ -91,7 +92,7 @@ async function getVitestBuildOptions(options, baseBuildOptions) {
91
92
  externalDependencies: ['vitest', '@vitest/browser/context'],
92
93
  };
93
94
  buildOptions.polyfills = (0, options_1.injectTestingPolyfills)(buildOptions.polyfills);
94
- const testBedInitContents = createTestBedInitVirtualFile(providersFile, projectSourceRoot);
95
+ const testBedInitContents = createTestBedInitVirtualFile(providersFile, projectSourceRoot, buildOptions.polyfills);
95
96
  return {
96
97
  buildOptions,
97
98
  virtualFiles: {
@@ -148,7 +148,7 @@ class VitestExecutor {
148
148
  reporters: reporters ?? ['default'],
149
149
  outputFile,
150
150
  watch,
151
- coverage: generateCoverageOption(codeCoverage),
151
+ coverage: generateCoverageOption(codeCoverage, this.projectName),
152
152
  ...debugOptions,
153
153
  }, {
154
154
  server: {
@@ -161,7 +161,7 @@ class VitestExecutor {
161
161
  }
162
162
  }
163
163
  exports.VitestExecutor = VitestExecutor;
164
- function generateCoverageOption(codeCoverage) {
164
+ function generateCoverageOption(codeCoverage, projectName) {
165
165
  if (!codeCoverage) {
166
166
  return {
167
167
  enabled: false,
@@ -170,6 +170,7 @@ function generateCoverageOption(codeCoverage) {
170
170
  return {
171
171
  enabled: true,
172
172
  excludeAfterRemap: true,
173
+ reportsDirectory: (0, path_1.toPosixPath)(node_path_1.default.join('coverage', projectName)),
173
174
  // Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures
174
175
  ...(codeCoverage.exclude ? { exclude: codeCoverage.exclude } : {}),
175
176
  ...(codeCoverage.reporters
@@ -53,6 +53,11 @@ export type Schema = {
53
53
  * within) and file paths (includes the corresponding `.spec` file if one exists).
54
54
  */
55
55
  include?: string[];
56
+ /**
57
+ * Lists all discovered test files and exits the process without building or executing the
58
+ * tests.
59
+ */
60
+ listTests?: boolean;
56
61
  /**
57
62
  * Specifies a file path for the test report, applying only to the first reporter. To
58
63
  * configure output files for multiple reporters, use the tuple format `['reporter-name', {
@@ -148,6 +148,11 @@
148
148
  "type": "boolean",
149
149
  "description": "Shows build progress information in the console. Defaults to the `progress` setting of the specified `buildTarget`."
150
150
  },
151
+ "listTests": {
152
+ "type": "boolean",
153
+ "description": "Lists all discovered test files and exits the process without building or executing the tests.",
154
+ "default": false
155
+ },
151
156
  "dumpVirtualFiles": {
152
157
  "type": "boolean",
153
158
  "description": "Dumps build output files to the `.angular/cache` directory for debugging purposes.",
@@ -5,4 +5,28 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
- export { findTests, getTestEntrypoints } from '../karma/find-tests';
8
+ /**
9
+ * Finds all test files in the project.
10
+ *
11
+ * @param include Glob patterns of files to include.
12
+ * @param exclude Glob patterns of files to exclude.
13
+ * @param workspaceRoot The absolute path to the workspace root.
14
+ * @param projectSourceRoot The absolute path to the project's source root.
15
+ * @returns A unique set of absolute paths to all test files.
16
+ */
17
+ export declare function findTests(include: string[], exclude: string[], workspaceRoot: string, projectSourceRoot: string): Promise<string[]>;
18
+ interface TestEntrypointsOptions {
19
+ projectSourceRoot: string;
20
+ workspaceRoot: string;
21
+ removeTestExtension?: boolean;
22
+ }
23
+ /**
24
+ * Generates unique, dash-delimited bundle names for a set of test files.
25
+ * This is used to create distinct output files for each test.
26
+ *
27
+ * @param testFiles An array of absolute paths to test files.
28
+ * @param options Configuration options for generating entry points.
29
+ * @returns A map where keys are the generated unique bundle names and values are the original file paths.
30
+ */
31
+ export declare function getTestEntrypoints(testFiles: string[], { projectSourceRoot, workspaceRoot, removeTestExtension }: TestEntrypointsOptions): Map<string, string>;
32
+ export {};
@@ -7,8 +7,197 @@
7
7
  * found in the LICENSE file at https://angular.dev/license
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.getTestEntrypoints = exports.findTests = void 0;
11
- // TODO: This should eventually contain the implementations for these
12
- var find_tests_1 = require("../karma/find-tests");
13
- Object.defineProperty(exports, "findTests", { enumerable: true, get: function () { return find_tests_1.findTests; } });
14
- Object.defineProperty(exports, "getTestEntrypoints", { enumerable: true, get: function () { return find_tests_1.getTestEntrypoints; } });
10
+ exports.findTests = findTests;
11
+ exports.getTestEntrypoints = getTestEntrypoints;
12
+ const node_fs_1 = require("node:fs");
13
+ const node_path_1 = require("node:path");
14
+ const tinyglobby_1 = require("tinyglobby");
15
+ const path_1 = require("../../utils/path");
16
+ /**
17
+ * Finds all test files in the project.
18
+ *
19
+ * @param include Glob patterns of files to include.
20
+ * @param exclude Glob patterns of files to exclude.
21
+ * @param workspaceRoot The absolute path to the workspace root.
22
+ * @param projectSourceRoot The absolute path to the project's source root.
23
+ * @returns A unique set of absolute paths to all test files.
24
+ */
25
+ async function findTests(include, exclude, workspaceRoot, projectSourceRoot) {
26
+ const staticMatches = new Set();
27
+ const dynamicPatterns = [];
28
+ const normalizedExcludes = exclude.map((p) => normalizePattern(p, workspaceRoot, projectSourceRoot));
29
+ // 1. Separate static and dynamic patterns
30
+ for (const pattern of include) {
31
+ const normalized = normalizePattern(pattern, workspaceRoot, projectSourceRoot);
32
+ if ((0, tinyglobby_1.isDynamicPattern)(normalized)) {
33
+ dynamicPatterns.push(normalized);
34
+ }
35
+ else {
36
+ const result = await handleStaticPattern(normalized, projectSourceRoot);
37
+ if (Array.isArray(result)) {
38
+ result.forEach((file) => staticMatches.add(file));
39
+ }
40
+ else {
41
+ // It was a static path that didn't resolve to a spec, treat as dynamic
42
+ dynamicPatterns.push(result);
43
+ }
44
+ }
45
+ }
46
+ // 2. Execute a single glob for all dynamic patterns
47
+ if (dynamicPatterns.length > 0) {
48
+ const globMatches = await (0, tinyglobby_1.glob)(dynamicPatterns, {
49
+ cwd: projectSourceRoot,
50
+ absolute: true,
51
+ ignore: ['**/node_modules/**', ...normalizedExcludes],
52
+ });
53
+ for (const match of globMatches) {
54
+ staticMatches.add(match);
55
+ }
56
+ }
57
+ // 3. Combine and de-duplicate results
58
+ return [...staticMatches];
59
+ }
60
+ /**
61
+ * Generates unique, dash-delimited bundle names for a set of test files.
62
+ * This is used to create distinct output files for each test.
63
+ *
64
+ * @param testFiles An array of absolute paths to test files.
65
+ * @param options Configuration options for generating entry points.
66
+ * @returns A map where keys are the generated unique bundle names and values are the original file paths.
67
+ */
68
+ function getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot, removeTestExtension }) {
69
+ const seen = new Set();
70
+ const roots = [projectSourceRoot, workspaceRoot];
71
+ return new Map(Array.from(testFiles, (testFile) => {
72
+ const fileName = generateNameFromPath(testFile, roots, !!removeTestExtension);
73
+ const baseName = `spec-${fileName}`;
74
+ let uniqueName = baseName;
75
+ let suffix = 2;
76
+ while (seen.has(uniqueName)) {
77
+ uniqueName = `${baseName}-${suffix}`.replace(/([^\w](?:spec|test))-([\d]+)$/, '-$2$1');
78
+ ++suffix;
79
+ }
80
+ seen.add(uniqueName);
81
+ return [uniqueName, testFile];
82
+ }));
83
+ }
84
+ /**
85
+ * Generates a unique, dash-delimited name from a file path.
86
+ * This is used to create a consistent and readable bundle name for a given test file.
87
+ * @param testFile The absolute path to the test file.
88
+ * @param roots An array of root paths to remove from the beginning of the test file path.
89
+ * @param removeTestExtension Whether to remove the `.spec` or `.test` extension from the result.
90
+ * @returns A dash-cased name derived from the relative path of the test file.
91
+ */
92
+ function generateNameFromPath(testFile, roots, removeTestExtension) {
93
+ const relativePath = removeRoots(testFile, roots);
94
+ let startIndex = 0;
95
+ // Skip leading dots and slashes
96
+ while (startIndex < relativePath.length && /^[./\\]$/.test(relativePath[startIndex])) {
97
+ startIndex++;
98
+ }
99
+ let endIndex = relativePath.length;
100
+ if (removeTestExtension) {
101
+ const match = relativePath.match(/\.(spec|test)\.[^.]+$/);
102
+ if (match?.index) {
103
+ endIndex = match.index;
104
+ }
105
+ }
106
+ else {
107
+ const extIndex = relativePath.lastIndexOf('.');
108
+ if (extIndex > startIndex) {
109
+ endIndex = extIndex;
110
+ }
111
+ }
112
+ // Build the final string in a single pass
113
+ let result = '';
114
+ for (let i = startIndex; i < endIndex; i++) {
115
+ const char = relativePath[i];
116
+ result += char === '/' || char === '\\' ? '-' : char;
117
+ }
118
+ return result;
119
+ }
120
+ const removeLeadingSlash = (pattern) => {
121
+ if (pattern.charAt(0) === '/') {
122
+ return pattern.substring(1);
123
+ }
124
+ return pattern;
125
+ };
126
+ const removeRelativeRoot = (path, root) => {
127
+ if (path.startsWith(root)) {
128
+ return path.substring(root.length);
129
+ }
130
+ return path;
131
+ };
132
+ /**
133
+ * Removes potential root paths from a file path, returning a relative path.
134
+ * If no root path matches, it returns the file's basename.
135
+ */
136
+ function removeRoots(path, roots) {
137
+ for (const root of roots) {
138
+ if (path.startsWith(root)) {
139
+ return path.substring(root.length);
140
+ }
141
+ }
142
+ return (0, node_path_1.basename)(path);
143
+ }
144
+ /**
145
+ * Normalizes a glob pattern by converting it to a POSIX path, removing leading slashes,
146
+ * and making it relative to the project source root.
147
+ *
148
+ * @param pattern The glob pattern to normalize.
149
+ * @param workspaceRoot The absolute path to the workspace root.
150
+ * @param projectSourceRoot The absolute path to the project's source root.
151
+ * @returns A normalized glob pattern.
152
+ */
153
+ function normalizePattern(pattern, workspaceRoot, projectSourceRoot) {
154
+ // normalize pattern, glob lib only accepts forward slashes
155
+ let normalizedPattern = (0, path_1.toPosixPath)(pattern);
156
+ normalizedPattern = removeLeadingSlash(normalizedPattern);
157
+ const relativeProjectRoot = (0, path_1.toPosixPath)((0, node_path_1.relative)(workspaceRoot, projectSourceRoot) + '/');
158
+ // remove relativeProjectRoot to support relative paths from root
159
+ // such paths are easy to get when running scripts via IDEs
160
+ return removeRelativeRoot(normalizedPattern, relativeProjectRoot);
161
+ }
162
+ /**
163
+ * Handles static (non-glob) patterns by attempting to resolve them to a directory
164
+ * of spec files or a corresponding `.spec` file.
165
+ *
166
+ * @param pattern The static path pattern.
167
+ * @param projectSourceRoot The absolute path to the project's source root.
168
+ * @returns A promise that resolves to either an array of found spec files, a new glob pattern,
169
+ * or the original pattern if no special handling was applied.
170
+ */
171
+ async function handleStaticPattern(pattern, projectSourceRoot) {
172
+ const fullPath = (0, node_path_1.join)(projectSourceRoot, pattern);
173
+ if (await isDirectory(fullPath)) {
174
+ return `${pattern}/**/*.spec.@(ts|tsx)`;
175
+ }
176
+ const fileExt = (0, node_path_1.extname)(pattern);
177
+ // Replace extension to `.spec.ext`. Example: `src/app/app.component.ts`-> `src/app/app.component.spec.ts`
178
+ const potentialSpec = (0, node_path_1.join)(projectSourceRoot, (0, node_path_1.dirname)(pattern), `${(0, node_path_1.basename)(pattern, fileExt)}.spec${fileExt}`);
179
+ if (await exists(potentialSpec)) {
180
+ return [potentialSpec];
181
+ }
182
+ return pattern;
183
+ }
184
+ /** Checks if a path exists and is a directory. */
185
+ async function isDirectory(path) {
186
+ try {
187
+ const stats = await node_fs_1.promises.stat(path);
188
+ return stats.isDirectory();
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ }
194
+ /** Checks if a path exists on the file system. */
195
+ async function exists(path) {
196
+ try {
197
+ await node_fs_1.promises.access(path, node_fs_1.constants.F_OK);
198
+ return true;
199
+ }
200
+ catch {
201
+ return false;
202
+ }
203
+ }
@@ -30,7 +30,7 @@ function replaceBootstrap(getTypeChecker) {
30
30
  if (target.text === PLATFORM_BROWSER_DYNAMIC_NAME) {
31
31
  if (!bootstrapNamespace) {
32
32
  bootstrapNamespace = nodeFactory.createUniqueName('__NgCli_bootstrap_');
33
- bootstrapImport = nodeFactory.createImportDeclaration(undefined, nodeFactory.createImportClause(false, undefined, nodeFactory.createNamespaceImport(bootstrapNamespace)), nodeFactory.createStringLiteral('@angular/platform-browser'));
33
+ bootstrapImport = nodeFactory.createImportDeclaration(undefined, nodeFactory.createImportClause(undefined, undefined, nodeFactory.createNamespaceImport(bootstrapNamespace)), nodeFactory.createStringLiteral('@angular/platform-browser'));
34
34
  }
35
35
  replacedNodes.push(target);
36
36
  return nodeFactory.updateCallExpression(node, nodeFactory.createPropertyAccessExpression(bootstrapNamespace, 'platformBrowser'), node.typeArguments, node.arguments);
@@ -141,7 +141,7 @@ function visitComponentMetadata(nodeFactory, node, styleReplacements, resourceIm
141
141
  function createResourceImport(nodeFactory, url, resourceImportDeclarations) {
142
142
  const urlLiteral = nodeFactory.createStringLiteral(url);
143
143
  const importName = nodeFactory.createIdentifier(`__NG_CLI_RESOURCE__${resourceImportDeclarations.length}`);
144
- resourceImportDeclarations.push(nodeFactory.createImportDeclaration(undefined, nodeFactory.createImportClause(false, importName, undefined), urlLiteral));
144
+ resourceImportDeclarations.push(nodeFactory.createImportDeclaration(undefined, nodeFactory.createImportClause(undefined, importName, undefined), urlLiteral));
145
145
  return importName;
146
146
  }
147
147
  function getDecoratorOrigin(decorator, typeChecker) {
@@ -55,11 +55,10 @@ exports.LessStylesheetLanguage = Object.freeze({
55
55
  componentFilter: /^less;/,
56
56
  fileFilter: /\.less$/,
57
57
  process(data, file, _, options, build) {
58
- return compileString(data, file, options, build.resolve.bind(build),
59
- /* unsafeInlineJavaScript */ false);
58
+ return compileString(data, file, options, build.resolve.bind(build));
60
59
  },
61
60
  });
62
- async function compileString(data, filename, options, resolver, unsafeInlineJavaScript) {
61
+ async function compileString(data, filename, options, resolver) {
63
62
  try {
64
63
  lessPreprocessor ??= (await Promise.resolve().then(() => __importStar(require('less')))).default;
65
64
  }
@@ -120,7 +119,6 @@ async function compileString(data, filename, options, resolver, unsafeInlineJava
120
119
  paths: options.includePaths,
121
120
  plugins: [resolverPlugin],
122
121
  rewriteUrls: 'all',
123
- javascriptEnabled: unsafeInlineJavaScript,
124
122
  sourceMap: options.sourcemap
125
123
  ? {
126
124
  sourceMapFileInline: true,
@@ -137,28 +135,6 @@ async function compileString(data, filename, options, resolver, unsafeInlineJava
137
135
  catch (error) {
138
136
  if (isLessException(error)) {
139
137
  const location = convertExceptionLocation(error);
140
- // Retry with a warning for less files requiring the deprecated inline JavaScript option
141
- if (error.message.includes('Inline JavaScript is not enabled.')) {
142
- const withJsResult = await compileString(data, filename, options, resolver,
143
- /* unsafeInlineJavaScript */ true);
144
- withJsResult.warnings = [
145
- {
146
- text: 'Deprecated inline execution of JavaScript has been enabled ("javascriptEnabled")',
147
- location,
148
- notes: [
149
- {
150
- location: null,
151
- text: 'JavaScript found within less stylesheets may be executed at build time. [https://lesscss.org/usage/#less-options]',
152
- },
153
- {
154
- location: null,
155
- text: 'Support for "javascriptEnabled" may be removed from the Angular CLI starting with Angular v19.',
156
- },
157
- ],
158
- },
159
- ];
160
- return withJsResult;
161
- }
162
138
  return {
163
139
  errors: [
164
140
  {
@@ -152,7 +152,8 @@ class StylesheetPluginFactory {
152
152
  postcssProcessor = postcss();
153
153
  const postCssPluginRequire = (0, node_module_1.createRequire)((0, node_path_1.dirname)(configPath) + '/');
154
154
  for (const [pluginName, pluginOptions] of config.plugins) {
155
- const plugin = postCssPluginRequire(pluginName);
155
+ const pluginMod = postCssPluginRequire(pluginName);
156
+ const plugin = pluginMod.__esModule ? pluginMod['default'] : pluginMod;
156
157
  if (typeof plugin !== 'function' || plugin.postcss !== true) {
157
158
  throw new Error(`Attempted to load invalid Postcss plugin: "${pluginName}"`);
158
159
  }
@@ -10,7 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.normalizeCacheOptions = normalizeCacheOptions;
11
11
  const node_path_1 = require("node:path");
12
12
  /** Version placeholder is replaced during the build process with actual package version */
13
- const VERSION = '21.0.0-next.4';
13
+ const VERSION = '21.0.0-next.5';
14
14
  function hasCacheMetadata(value) {
15
15
  return (!!value &&
16
16
  typeof value === 'object' &&