@angular/build 20.0.0-rc.0 → 20.0.0-rc.2

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": "20.0.0-rc.0",
3
+ "version": "20.0.0-rc.2",
4
4
  "description": "Official build system for Angular",
5
5
  "keywords": [
6
6
  "Angular CLI",
@@ -23,11 +23,11 @@
23
23
  "builders": "builders.json",
24
24
  "dependencies": {
25
25
  "@ampproject/remapping": "2.3.0",
26
- "@angular-devkit/architect": "0.2000.0-rc.0",
26
+ "@angular-devkit/architect": "0.2000.0-rc.2",
27
27
  "@babel/core": "7.27.1",
28
28
  "@babel/helper-annotate-as-pure": "7.27.1",
29
29
  "@babel/helper-split-export-declaration": "7.24.7",
30
- "@inquirer/confirm": "5.1.9",
30
+ "@inquirer/confirm": "5.1.10",
31
31
  "@vitejs/plugin-basic-ssl": "2.0.0",
32
32
  "beasties": "0.3.3",
33
33
  "browserslist": "^4.23.0",
@@ -42,8 +42,8 @@
42
42
  "picomatch": "4.0.2",
43
43
  "piscina": "5.0.0",
44
44
  "rollup": "4.40.2",
45
- "sass": "1.87.0",
46
- "semver": "7.7.1",
45
+ "sass": "1.88.0",
46
+ "semver": "7.7.2",
47
47
  "source-map-support": "0.5.21",
48
48
  "tinyglobby": "0.2.13",
49
49
  "vite": "6.3.5",
@@ -60,7 +60,7 @@
60
60
  "@angular/platform-browser": "^20.0.0 || ^20.0.0-next.0",
61
61
  "@angular/platform-server": "^20.0.0 || ^20.0.0-next.0",
62
62
  "@angular/service-worker": "^20.0.0 || ^20.0.0-next.0",
63
- "@angular/ssr": "^20.0.0-rc.0",
63
+ "@angular/ssr": "^20.0.0-rc.2",
64
64
  "karma": "^6.4.0",
65
65
  "less": "^4.2.0",
66
66
  "ng-packagr": "^20.0.0 || ^20.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@9.15.6",
115
+ "packageManager": "pnpm@9.15.9",
116
116
  "engines": {
117
117
  "node": "^20.11.1 || ^22.11.0 || >=24.0.0",
118
118
  "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
@@ -16,6 +16,7 @@ const node_crypto_1 = require("node:crypto");
16
16
  const node_module_1 = require("node:module");
17
17
  const node_path_1 = __importDefault(require("node:path"));
18
18
  const virtual_module_plugin_1 = require("../../tools/esbuild/virtual-module-plugin");
19
+ const error_1 = require("../../utils/error");
19
20
  const load_esm_1 = require("../../utils/load-esm");
20
21
  const application_1 = require("../application");
21
22
  const results_1 = require("../application/results");
@@ -27,6 +28,7 @@ const options_1 = require("./options");
27
28
  /**
28
29
  * @experimental Direct usage of this function is considered experimental.
29
30
  */
31
+ // eslint-disable-next-line max-lines-per-function
30
32
  async function* execute(options, context, extensions = {}) {
31
33
  // Determine project name from builder context target
32
34
  const projectName = context.target?.project;
@@ -55,7 +57,19 @@ async function* execute(options, context, extensions = {}) {
55
57
  }
56
58
  const entryPoints = (0, find_tests_1.getTestEntrypoints)(testFiles, { projectSourceRoot, workspaceRoot });
57
59
  entryPoints.set('init-testbed', 'angular:test-bed-init');
58
- const { startVitest } = await (0, load_esm_1.loadEsmModule)('vitest/node');
60
+ let vitestNodeModule;
61
+ try {
62
+ vitestNodeModule = await (0, load_esm_1.loadEsmModule)('vitest/node');
63
+ }
64
+ catch (error) {
65
+ (0, error_1.assertIsError)(error);
66
+ if (error.code !== 'ERR_MODULE_NOT_FOUND') {
67
+ throw error;
68
+ }
69
+ context.logger.error('The `vitest` package was not found. Please install the package and rerun the test command.');
70
+ return;
71
+ }
72
+ const { startVitest } = vitestNodeModule;
59
73
  // Setup test file build options based on application build target options
60
74
  const buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), await context.getBuilderNameForTarget(normalizedOptions.buildTarget)));
61
75
  if (buildTargetOptions.polyfills?.includes('zone.js')) {
@@ -65,6 +79,7 @@ async function* execute(options, context, extensions = {}) {
65
79
  const buildOptions = {
66
80
  ...buildTargetOptions,
67
81
  watch: normalizedOptions.watch,
82
+ incrementalResults: normalizedOptions.watch,
68
83
  outputPath,
69
84
  index: false,
70
85
  browser: undefined,
@@ -89,15 +104,28 @@ async function* execute(options, context, extensions = {}) {
89
104
  loadContent: async () => {
90
105
  const contents = [
91
106
  // Initialize the Angular testing environment
107
+ `import { NgModule } from '@angular/core';`,
92
108
  `import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing';`,
93
109
  `import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`,
94
110
  `import { beforeEach, afterEach } from 'vitest';`,
95
111
  '',
112
+ normalizedOptions.providersFile
113
+ ? `import providers from './${node_path_1.default
114
+ .relative(projectSourceRoot, normalizedOptions.providersFile)
115
+ .replace(/.[mc]?ts$/, '')
116
+ .replace(/\\/g, '/')}'`
117
+ : 'const providers = [];',
118
+ '',
96
119
  // Same as https://github.com/angular/angular/blob/05a03d3f975771bb59c7eefd37c01fa127ee2229/packages/core/testing/src/test_hooks.ts#L21-L29
97
120
  `beforeEach(getCleanupHook(false));`,
98
121
  `afterEach(getCleanupHook(true));`,
99
122
  '',
100
- `getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {`,
123
+ `@NgModule({`,
124
+ ` providers,`,
125
+ `})`,
126
+ `export class TestModule {}`,
127
+ '',
128
+ `getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {`,
101
129
  ` errorOnUnknownElements: true,`,
102
130
  ` errorOnUnknownProperties: true,`,
103
131
  '});',
@@ -112,60 +140,58 @@ async function* execute(options, context, extensions = {}) {
112
140
  extensions.codePlugins.unshift(virtualTestBedInit);
113
141
  let instance;
114
142
  // Setup vitest browser options if configured
115
- let browser;
116
- if (normalizedOptions.browsers) {
117
- const provider = findBrowserProvider(projectSourceRoot);
118
- if (!provider) {
119
- context.logger.error('The "browsers" option requires either "playwright" or "webdriverio" to be installed within the project.' +
120
- ' Please install one of these packages and rerun the test command.');
121
- return { success: false };
122
- }
123
- browser = {
124
- enabled: true,
125
- provider,
126
- instances: normalizedOptions.browsers.map((browserName) => ({
127
- browser: browserName,
128
- })),
129
- };
130
- }
131
- for await (const result of (0, application_1.buildApplicationInternal)(buildOptions, context, extensions)) {
132
- if (result.kind === results_1.ResultKind.Failure) {
133
- continue;
134
- }
135
- else if (result.kind !== results_1.ResultKind.Full) {
136
- node_assert_1.default.fail('A full build result is required from the application builder.');
143
+ const { browser, errors } = setupBrowserConfiguration(normalizedOptions.browsers, projectSourceRoot);
144
+ if (errors?.length) {
145
+ errors.forEach((error) => context.logger.error(error));
146
+ return { success: false };
147
+ }
148
+ // Add setup file entries for TestBed initialization and project polyfills
149
+ const setupFiles = ['init-testbed.js'];
150
+ if (buildTargetOptions?.polyfills?.length) {
151
+ setupFiles.push('polyfills.js');
152
+ }
153
+ try {
154
+ for await (const result of (0, application_1.buildApplicationInternal)(buildOptions, context, extensions)) {
155
+ if (result.kind === results_1.ResultKind.Failure) {
156
+ continue;
157
+ }
158
+ else if (result.kind !== results_1.ResultKind.Full && result.kind !== results_1.ResultKind.Incremental) {
159
+ node_assert_1.default.fail('A full and/or incremental build result is required from the application builder.');
160
+ }
161
+ (0, node_assert_1.default)(result.files, 'Builder did not provide result files.');
162
+ await (0, application_builder_1.writeTestFiles)(result.files, outputPath);
163
+ instance ??= await startVitest('test', undefined /* cliFilters */, undefined /* options */, {
164
+ test: {
165
+ root: outputPath,
166
+ globals: true,
167
+ setupFiles,
168
+ // Use `jsdom` if no browsers are explicitly configured.
169
+ // `node` is effectively no "environment" and the default.
170
+ environment: browser ? 'node' : 'jsdom',
171
+ watch: normalizedOptions.watch,
172
+ browser,
173
+ reporters: normalizedOptions.reporters ?? ['default'],
174
+ coverage: {
175
+ enabled: normalizedOptions.codeCoverage,
176
+ exclude: normalizedOptions.codeCoverageExclude,
177
+ excludeAfterRemap: true,
178
+ },
179
+ },
180
+ });
181
+ // Check if all the tests pass to calculate the result
182
+ const testModules = instance.state.getTestModules();
183
+ yield { success: testModules.every((testModule) => testModule.ok()) };
137
184
  }
138
- (0, node_assert_1.default)(result.files, 'Builder did not provide result files.');
139
- await (0, application_builder_1.writeTestFiles)(result.files, outputPath);
140
- const setupFiles = ['init-testbed.js'];
141
- if (buildTargetOptions?.polyfills?.length) {
142
- setupFiles.push('polyfills.js');
185
+ }
186
+ finally {
187
+ if (normalizedOptions.watch) {
188
+ // Vitest will automatically close if not using watch mode
189
+ await instance?.close();
143
190
  }
144
- instance ??= await startVitest('test', undefined /* cliFilters */, undefined /* options */, {
145
- test: {
146
- root: outputPath,
147
- setupFiles,
148
- // Use `jsdom` if no browsers are explicitly configured.
149
- // `node` is effectively no "environment" and the default.
150
- environment: browser ? 'node' : 'jsdom',
151
- watch: normalizedOptions.watch,
152
- browser,
153
- reporters: normalizedOptions.reporters ?? ['default'],
154
- coverage: {
155
- enabled: normalizedOptions.codeCoverage,
156
- exclude: normalizedOptions.codeCoverageExclude,
157
- excludeAfterRemap: true,
158
- },
159
- },
160
- });
161
- // Check if all the tests pass to calculate the result
162
- const testModules = instance.state.getTestModules();
163
- yield { success: testModules.every((testModule) => testModule.ok()) };
164
191
  }
165
192
  }
166
- function findBrowserProvider(projectSourceRoot) {
167
- const projectResolver = (0, node_module_1.createRequire)(projectSourceRoot + '/').resolve;
168
- // These must be installed in the project to be used
193
+ function findBrowserProvider(projectResolver) {
194
+ // One of these must be installed in the project to use browser testing
169
195
  const vitestBuiltinProviders = ['playwright', 'webdriverio'];
170
196
  for (const providerName of vitestBuiltinProviders) {
171
197
  try {
@@ -175,3 +201,35 @@ function findBrowserProvider(projectSourceRoot) {
175
201
  catch { }
176
202
  }
177
203
  }
204
+ function setupBrowserConfiguration(browsers, projectSourceRoot) {
205
+ if (browsers === undefined) {
206
+ return {};
207
+ }
208
+ const projectResolver = (0, node_module_1.createRequire)(projectSourceRoot + '/').resolve;
209
+ let errors;
210
+ try {
211
+ projectResolver('@vitest/browser');
212
+ }
213
+ catch {
214
+ errors ??= [];
215
+ errors.push('The "browsers" option requires the "@vitest/browser" package to be installed within the project.' +
216
+ ' Please install this package and rerun the test command.');
217
+ }
218
+ const provider = findBrowserProvider(projectResolver);
219
+ if (!provider) {
220
+ errors ??= [];
221
+ errors.push('The "browsers" option requires either "playwright" or "webdriverio" to be installed within the project.' +
222
+ ' Please install one of these packages and rerun the test command.');
223
+ }
224
+ if (errors) {
225
+ return { errors };
226
+ }
227
+ const browser = {
228
+ enabled: true,
229
+ provider,
230
+ instances: browsers.map((browserName) => ({
231
+ browser: browserName,
232
+ })),
233
+ };
234
+ return { browser };
235
+ }
@@ -22,5 +22,6 @@ export declare function normalizeOptions(context: BuilderContext, projectName: s
22
22
  tsConfig: string;
23
23
  reporters: string[] | undefined;
24
24
  browsers: string[] | undefined;
25
- watch: boolean;
25
+ watch: boolean | undefined;
26
+ providersFile: string | undefined;
26
27
  }>;
@@ -26,7 +26,7 @@ async function normalizeOptions(context, projectName, options) {
26
26
  // Target specifier defaults to the current project's build target using a development configuration
27
27
  const buildTargetSpecifier = options.buildTarget ?? `::development`;
28
28
  const buildTarget = (0, architect_1.targetFromTargetString)(buildTargetSpecifier, projectName, 'build');
29
- const { codeCoverage, codeCoverageExclude, tsConfig, runner, reporters, browsers } = options;
29
+ const { codeCoverage, codeCoverageExclude, tsConfig, runner, reporters, browsers, watch } = options;
30
30
  return {
31
31
  // Project/workspace information
32
32
  workspaceRoot,
@@ -43,8 +43,8 @@ async function normalizeOptions(context, projectName, options) {
43
43
  tsConfig,
44
44
  reporters,
45
45
  browsers,
46
- // TODO: Implement watch support
47
- watch: false,
46
+ watch,
47
+ providersFile: options.providersFile && node_path_1.default.join(workspaceRoot, options.providersFile),
48
48
  };
49
49
  }
50
50
  /**
@@ -34,6 +34,11 @@ export type Schema = {
34
34
  * instead.
35
35
  */
36
36
  include?: string[];
37
+ /**
38
+ * TypeScript file that exports an array of Angular providers to use during test execution.
39
+ * The array must be a default export.
40
+ */
41
+ providersFile?: string;
37
42
  /**
38
43
  * Test runner reporters to use. Directly passed to the test runner.
39
44
  */
@@ -65,6 +65,11 @@
65
65
  "items": {
66
66
  "type": "string"
67
67
  }
68
+ },
69
+ "providersFile": {
70
+ "type": "string",
71
+ "description": "TypeScript file that exports an array of Angular providers to use during test execution. The array must be a default export.",
72
+ "minLength": 1
68
73
  }
69
74
  },
70
75
  "additionalProperties": false,
@@ -93,7 +93,7 @@ async function loadProxyConfiguration(root, proxyConfig) {
93
93
  }
94
94
  catch (e) {
95
95
  (0, error_1.assertIsError)(e);
96
- if (e.code === 'ERR_REQUIRE_ESM') {
96
+ if (e.code === 'ERR_REQUIRE_ESM' || e.code === 'ERR_REQUIRE_ASYNC_MODULE') {
97
97
  // Load the ESM configuration file using the TypeScript dynamic import workaround.
98
98
  // Once TypeScript provides support for keeping the dynamic import this workaround can be
99
99
  // changed to a direct dynamic import.
@@ -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 = '20.0.0-rc.0';
13
+ const VERSION = '20.0.0-rc.2';
14
14
  function hasCacheMetadata(value) {
15
15
  return (!!value &&
16
16
  typeof value === 'object' &&