@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 +8 -8
- package/src/builders/karma/application_builder.js +5 -1
- package/src/builders/karma/coverage.js +1 -1
- package/src/builders/karma/find-tests.d.ts +1 -9
- package/src/builders/karma/find-tests.js +6 -106
- package/src/builders/unit-test/builder.js +10 -0
- package/src/builders/unit-test/options.d.ts +1 -0
- package/src/builders/unit-test/options.js +1 -0
- package/src/builders/unit-test/runners/vitest/build-options.js +5 -4
- package/src/builders/unit-test/runners/vitest/executor.js +3 -2
- package/src/builders/unit-test/schema.d.ts +5 -0
- package/src/builders/unit-test/schema.json +5 -0
- package/src/builders/unit-test/test-discovery.d.ts +25 -1
- package/src/builders/unit-test/test-discovery.js +194 -5
- package/src/tools/angular/transformers/jit-bootstrap-transformer.js +1 -1
- package/src/tools/angular/transformers/jit-resource-transformer.js +1 -1
- package/src/tools/esbuild/stylesheets/less-language.js +2 -26
- package/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.js +2 -1
- package/src/utils/normalize-cache.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular/build",
|
|
3
|
-
"version": "21.0.0-next.
|
|
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.
|
|
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.
|
|
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.
|
|
45
|
-
"sass": "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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
11
|
-
exports
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
13
|
+
const VERSION = '21.0.0-next.5';
|
|
14
14
|
function hasCacheMetadata(value) {
|
|
15
15
|
return (!!value &&
|
|
16
16
|
typeof value === 'object' &&
|