@angular-devkit/build-angular 17.1.0-next.2 → 17.1.0-rc.0

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.
Files changed (57) hide show
  1. package/builders.json +5 -0
  2. package/package.json +24 -20
  3. package/src/builders/app-shell/index.js +7 -0
  4. package/src/builders/application/build-action.d.ts +4 -3
  5. package/src/builders/application/build-action.js +8 -5
  6. package/src/builders/application/execute-build.js +10 -4
  7. package/src/builders/application/index.d.ts +20 -10
  8. package/src/builders/application/index.js +38 -26
  9. package/src/builders/application/options.d.ts +11 -3
  10. package/src/builders/application/options.js +42 -30
  11. package/src/builders/application/schema.d.ts +32 -2
  12. package/src/builders/application/schema.json +40 -2
  13. package/src/builders/browser-esbuild/index.js +8 -4
  14. package/src/builders/dev-server/vite-server.js +7 -13
  15. package/src/builders/jest/index.js +2 -2
  16. package/src/builders/prerender/index.js +7 -0
  17. package/src/builders/ssr-dev-server/index.js +17 -31
  18. package/src/builders/web-test-runner/builder-status-warnings.d.ts +11 -0
  19. package/src/builders/web-test-runner/builder-status-warnings.js +46 -0
  20. package/src/builders/web-test-runner/index.d.ts +10 -0
  21. package/src/builders/web-test-runner/index.js +151 -0
  22. package/src/builders/web-test-runner/jasmine_runner.js +88 -0
  23. package/src/builders/web-test-runner/options.d.ts +24 -0
  24. package/src/builders/web-test-runner/options.js +26 -0
  25. package/src/builders/web-test-runner/schema.d.ts +188 -0
  26. package/src/builders/web-test-runner/schema.js +15 -0
  27. package/src/builders/web-test-runner/schema.json +291 -0
  28. package/src/builders/web-test-runner/test_page.html +40 -0
  29. package/src/tools/esbuild/angular/angular-host.js +1 -1
  30. package/src/tools/esbuild/angular/compiler-plugin.js +10 -26
  31. package/src/tools/esbuild/angular/component-stylesheets.d.ts +3 -6
  32. package/src/tools/esbuild/angular/component-stylesheets.js +46 -60
  33. package/src/tools/esbuild/angular/jit-plugin-callbacks.js +2 -2
  34. package/src/tools/esbuild/bundler-context.d.ts +1 -1
  35. package/src/tools/esbuild/bundler-context.js +18 -2
  36. package/src/tools/esbuild/cache.d.ts +88 -0
  37. package/src/tools/esbuild/cache.js +92 -0
  38. package/src/tools/esbuild/compiler-plugin-options.js +1 -1
  39. package/src/tools/esbuild/index-html-generator.js +3 -1
  40. package/src/tools/esbuild/javascript-transformer-worker.d.ts +2 -2
  41. package/src/tools/esbuild/javascript-transformer-worker.js +12 -5
  42. package/src/tools/esbuild/javascript-transformer.d.ts +3 -1
  43. package/src/tools/esbuild/javascript-transformer.js +42 -17
  44. package/src/tools/esbuild/stylesheets/bundle-options.d.ts +1 -1
  45. package/src/tools/esbuild/stylesheets/sass-language.js +3 -12
  46. package/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.js +9 -1
  47. package/src/tools/esbuild/utils.d.ts +2 -2
  48. package/src/tools/esbuild/utils.js +61 -74
  49. package/src/tools/sass/lexer.d.ts +0 -11
  50. package/src/tools/sass/lexer.js +1 -87
  51. package/src/utils/index-file/index-html-generator.js +15 -28
  52. package/src/utils/index.d.ts +1 -0
  53. package/src/utils/index.js +1 -0
  54. package/src/{builders/dev-server → utils}/load-proxy-config.js +2 -2
  55. package/src/{builders/jest → utils}/test-files.d.ts +1 -2
  56. package/src/{builders/jest → utils}/test-files.js +3 -3
  57. /package/src/{builders/dev-server → utils}/load-proxy-config.d.ts +0 -0
@@ -220,8 +220,41 @@
220
220
  "default": []
221
221
  },
222
222
  "outputPath": {
223
- "type": "string",
224
- "description": "The full path for the new output directory, relative to the current workspace."
223
+ "description": "Specify the output path relative to workspace root.",
224
+ "oneOf": [
225
+ {
226
+ "type": "object",
227
+ "properties": {
228
+ "base": {
229
+ "type": "string",
230
+ "description": "Specify the output path relative to workspace root."
231
+ },
232
+ "browser": {
233
+ "type": "string",
234
+ "pattern": "^[-\\w\\.]*$",
235
+ "default": "browser",
236
+ "description": "The output directory name of your browser build within the output path base. Defaults to 'browser'."
237
+ },
238
+ "server": {
239
+ "type": "string",
240
+ "pattern": "^[-\\w\\.]*$",
241
+ "default": "server",
242
+ "description": "The output directory name of your server build within the output path base. Defaults to 'server'."
243
+ },
244
+ "media": {
245
+ "type": "string",
246
+ "pattern": "^[-\\w\\.]+$",
247
+ "default": "media",
248
+ "description": "The output directory name of your media files within the output browser directory. Defaults to 'media'."
249
+ }
250
+ },
251
+ "required": ["base"],
252
+ "additionalProperties": false
253
+ },
254
+ {
255
+ "type": "string"
256
+ }
257
+ ]
225
258
  },
226
259
  "aot": {
227
260
  "type": "boolean",
@@ -383,6 +416,11 @@
383
416
  "minLength": 1,
384
417
  "default": "index.html",
385
418
  "description": "The output path of the application's generated HTML index file. The full provided path will be used and will be considered relative to the application's configured output path."
419
+ },
420
+ "preloadInitial": {
421
+ "type": "boolean",
422
+ "default": true,
423
+ "description": "Generates 'preload', 'modulepreload', and 'preconnect' link elements for initial application files and resources."
386
424
  }
387
425
  },
388
426
  "required": ["input"]
@@ -31,13 +31,13 @@ async function* buildEsbuildBrowser(userOptions, context, infrastructureSettings
31
31
  (0, builder_status_warnings_1.logBuilderStatusWarnings)(userOptions, context);
32
32
  const normalizedOptions = normalizeOptions(userOptions);
33
33
  const { deleteOutputPath, outputPath } = normalizedOptions;
34
- const fullOutputPath = node_path_1.default.join(context.workspaceRoot, outputPath);
34
+ const fullOutputPath = node_path_1.default.join(context.workspaceRoot, outputPath.base);
35
35
  if (deleteOutputPath && infrastructureSettings?.write !== false) {
36
- await (0, utils_2.deleteOutputDir)(context.workspaceRoot, outputPath);
36
+ await (0, utils_2.deleteOutputDir)(context.workspaceRoot, outputPath.base);
37
37
  }
38
38
  for await (const result of (0, application_1.buildApplicationInternal)(normalizedOptions, context, {
39
39
  write: false,
40
- }, plugins)) {
40
+ }, plugins && { codePlugins: plugins })) {
41
41
  if (infrastructureSettings?.write !== false && result.outputFiles) {
42
42
  // Write output files
43
43
  await writeResultFiles(result.outputFiles, result.assetFiles, fullOutputPath);
@@ -55,11 +55,15 @@ async function* buildEsbuildBrowser(userOptions, context, infrastructureSettings
55
55
  }
56
56
  exports.buildEsbuildBrowser = buildEsbuildBrowser;
57
57
  function normalizeOptions(options) {
58
- const { main: browser, ngswConfigPath, serviceWorker, polyfills, ...otherOptions } = options;
58
+ const { main: browser, outputPath, ngswConfigPath, serviceWorker, polyfills, ...otherOptions } = options;
59
59
  return {
60
60
  browser,
61
61
  serviceWorker: serviceWorker ? ngswConfigPath : false,
62
62
  polyfills: typeof polyfills === 'string' ? [polyfills] : polyfills,
63
+ outputPath: {
64
+ base: outputPath,
65
+ browser: '',
66
+ },
63
67
  ...otherOptions,
64
68
  };
65
69
  }
@@ -51,7 +51,6 @@ const supported_browsers_1 = require("../../utils/supported-browsers");
51
51
  const webpack_browser_config_1 = require("../../utils/webpack-browser-config");
52
52
  const application_1 = require("../application");
53
53
  const browser_esbuild_1 = require("../browser-esbuild");
54
- const load_proxy_config_1 = require("./load-proxy-config");
55
54
  // eslint-disable-next-line max-lines-per-function
56
55
  async function* serveWithVite(serverOptions, builderName, context, transformers, extensions) {
57
56
  // Get the browser configuration from the target name.
@@ -97,7 +96,7 @@ async function* serveWithVite(serverOptions, builderName, context, transformers,
97
96
  // Always enable JIT linking to support applications built with and without AOT.
98
97
  // In a development environment the additional scope information does not
99
98
  // have a negative effect unlike production where final output size is relevant.
100
- { sourcemap: true, jit: true, thirdPartySourcemaps }, 1, true);
99
+ { sourcemap: true, jit: true, thirdPartySourcemaps }, 1);
101
100
  // Extract output index from options
102
101
  // TODO: Provide this info from the build results
103
102
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -122,14 +121,10 @@ async function* serveWithVite(serverOptions, builderName, context, transformers,
122
121
  deferred?.();
123
122
  });
124
123
  const build = builderName === '@angular-devkit/build-angular:browser-esbuild'
125
- ? browser_esbuild_1.buildEsbuildBrowser
126
- : application_1.buildApplicationInternal;
124
+ ? browser_esbuild_1.buildEsbuildBrowser.bind(undefined, browserOptions, context, { write: false }, extensions?.buildPlugins)
125
+ : application_1.buildApplicationInternal.bind(undefined, browserOptions, context, { write: false }, { codePlugins: extensions?.buildPlugins });
127
126
  // TODO: Switch this to an architect schedule call when infrastructure settings are supported
128
- for await (const result of build(
129
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
- browserOptions, context, {
131
- write: false,
132
- }, extensions?.buildPlugins)) {
127
+ for await (const result of build()) {
133
128
  (0, node_assert_1.default)(result.outputFiles, 'Builder did not provide result files.');
134
129
  // If build failed, nothing to serve
135
130
  if (!result.success) {
@@ -331,7 +326,7 @@ function analyzeResultFiles(normalizePath, htmlIndexPath, resultFiles, generated
331
326
  }
332
327
  // eslint-disable-next-line max-lines-per-function
333
328
  async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks, externalMetadata, ssr, prebundleTransformer, target, prebundleLoaderExtensions, extensionMiddleware, indexHtmlTransformer, thirdPartySourcemaps = false) {
334
- const proxy = await (0, load_proxy_config_1.loadProxyConfiguration)(serverOptions.workspaceRoot, serverOptions.proxyConfig, true);
329
+ const proxy = await (0, utils_2.loadProxyConfiguration)(serverOptions.workspaceRoot, serverOptions.proxyConfig, true);
335
330
  // dynamically import Vite for ESM compatibility
336
331
  const { normalizePath } = await (0, load_esm_1.loadEsmModule)('vite');
337
332
  // Path will not exist on disk and only used to provide separate path for Vite requests
@@ -532,11 +527,9 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
532
527
  return;
533
528
  }
534
529
  transformIndexHtmlAndAddHeaders(url, rawHtml, res, next, async (html) => {
535
- const protocol = serverOptions.ssl ? 'https' : 'http';
536
- const route = `${protocol}://${req.headers.host}${req.originalUrl}`;
537
530
  const { content } = await (0, render_page_1.renderPage)({
538
531
  document: html,
539
- route,
532
+ route: new URL(req.originalUrl ?? '/', server.resolvedUrls?.local[0]).toString(),
540
533
  serverContext: 'ssr',
541
534
  loadBundle: (uri) =>
542
535
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -622,6 +615,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
622
615
  }
623
616
  else {
624
617
  const { default: basicSslPlugin } = await Promise.resolve().then(() => __importStar(require('@vitejs/plugin-basic-ssl')));
618
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
625
619
  configuration.plugins ??= [];
626
620
  configuration.plugins.push(basicSslPlugin());
627
621
  }
@@ -35,10 +35,10 @@ const child_process_1 = require("child_process");
35
35
  const path = __importStar(require("path"));
36
36
  const util_1 = require("util");
37
37
  const color_1 = require("../../utils/color");
38
+ const test_files_1 = require("../../utils/test-files");
38
39
  const application_1 = require("../application");
39
40
  const schema_1 = require("../browser-esbuild/schema");
40
41
  const options_1 = require("./options");
41
- const test_files_1 = require("./test-files");
42
42
  const execFile = (0, util_1.promisify)(child_process_1.execFile);
43
43
  /** Main execution function for the Jest builder. */
44
44
  exports.default = (0, architect_1.createBuilder)(async (schema, context) => {
@@ -66,7 +66,7 @@ exports.default = (0, architect_1.createBuilder)(async (schema, context) => {
66
66
  };
67
67
  }
68
68
  // Build all the test files.
69
- const testFiles = await (0, test_files_1.findTestFiles)(options, context.workspaceRoot);
69
+ const testFiles = await (0, test_files_1.findTestFiles)(options.include, options.exclude, context.workspaceRoot);
70
70
  const jestGlobal = path.join(__dirname, 'jest-global.mjs');
71
71
  const initTestBed = path.join(__dirname, 'init-test-bed.mjs');
72
72
  const buildResult = await build(context, {
@@ -93,6 +93,13 @@ async function _scheduleBuilds(options, context) {
93
93
  serviceWorker: false,
94
94
  // todo: handle service worker augmentation
95
95
  });
96
+ if (browserTargetRun.info.builderName === '@angular-devkit/build-angular:application') {
97
+ return {
98
+ success: false,
99
+ error: '"@angular-devkit/build-angular:application" has built-in prerendering capabilities. ' +
100
+ 'The "prerender" option should be used instead.',
101
+ };
102
+ }
96
103
  const serverTargetRun = await context.scheduleTarget(serverTarget, {
97
104
  watch: false,
98
105
  });
@@ -36,8 +36,8 @@ const core_1 = require("@angular-devkit/core");
36
36
  const path_1 = require("path");
37
37
  const rxjs_1 = require("rxjs");
38
38
  const url = __importStar(require("url"));
39
- const error_1 = require("../../utils/error");
40
- const utils_1 = require("./utils");
39
+ const utils_1 = require("../../utils");
40
+ const utils_2 = require("./utils");
41
41
  /** Log messages to ignore and not rely to the logger */
42
42
  const IGNORED_STDOUT_MESSAGES = [
43
43
  'server listening on',
@@ -78,7 +78,7 @@ function execute(options, context) {
78
78
  DON'T USE IT FOR PRODUCTION!
79
79
  ****************************************************************************************
80
80
  `);
81
- return (0, rxjs_1.zip)(browserTargetRun, serverTargetRun, (0, utils_1.getAvailablePort)()).pipe((0, rxjs_1.switchMap)(([br, sr, nodeServerPort]) => {
81
+ return (0, rxjs_1.zip)(browserTargetRun, serverTargetRun, (0, utils_2.getAvailablePort)()).pipe((0, rxjs_1.switchMap)(([br, sr, nodeServerPort]) => {
82
82
  return (0, rxjs_1.combineLatest)([br.output, sr.output]).pipe(
83
83
  // This is needed so that if both server and browser emit close to each other
84
84
  // we only emit once. This typically happens on the first build.
@@ -101,7 +101,7 @@ function execute(options, context) {
101
101
  context.logger.info('\nCompiled successfully.');
102
102
  }
103
103
  }), (0, rxjs_1.debounce)(([builderOutput]) => builderOutput.success && !options.inspect
104
- ? (0, utils_1.waitUntilServerIsListening)(nodeServerPort)
104
+ ? (0, utils_2.waitUntilServerIsListening)(nodeServerPort)
105
105
  : rxjs_1.EMPTY), (0, rxjs_1.finalize)(() => {
106
106
  void br.stop();
107
107
  void sr.stop();
@@ -162,7 +162,7 @@ function startNodeServer(serverOutput, port, logger, inspectMode = false) {
162
162
  args.unshift('--inspect-brk');
163
163
  }
164
164
  return (0, rxjs_1.of)(null).pipe((0, rxjs_1.delay)(0), // Avoid EADDRINUSE error since it will cause the kill event to be finish.
165
- (0, rxjs_1.switchMap)(() => (0, utils_1.spawnAsObservable)('node', args, { env, shell: true })), (0, rxjs_1.tap)((res) => log({ stderr: res.stderr, stdout: res.stdout }, logger)), (0, rxjs_1.ignoreElements)(),
165
+ (0, rxjs_1.switchMap)(() => (0, utils_2.spawnAsObservable)('node', args, { env, shell: true })), (0, rxjs_1.tap)((res) => log({ stderr: res.stderr, stdout: res.stdout }, logger)), (0, rxjs_1.ignoreElements)(),
166
166
  // Emit a signal after the process has been started
167
167
  (0, rxjs_1.startWith)(undefined));
168
168
  }
@@ -171,7 +171,7 @@ async function initBrowserSync(browserSyncInstance, nodeServerPort, options, con
171
171
  return browserSyncInstance;
172
172
  }
173
173
  const { port: browserSyncPort, open, host, publicHost, proxyConfig } = options;
174
- const bsPort = browserSyncPort || (await (0, utils_1.getAvailablePort)());
174
+ const bsPort = browserSyncPort || (await (0, utils_2.getAvailablePort)());
175
175
  const bsOptions = {
176
176
  proxy: {
177
177
  target: `localhost:${nodeServerPort}`,
@@ -283,33 +283,19 @@ function getSslConfig(root, options) {
283
283
  return ssl;
284
284
  }
285
285
  async function getProxyConfig(root, proxyConfig) {
286
- const proxyPath = (0, path_1.resolve)(root, proxyConfig);
287
- let proxySettings;
288
- try {
289
- proxySettings = require(proxyPath);
290
- }
291
- catch (error) {
292
- (0, error_1.assertIsError)(error);
293
- if (error.code === 'MODULE_NOT_FOUND') {
294
- throw new Error(`Proxy config file ${proxyPath} does not exist.`);
295
- }
296
- throw error;
297
- }
298
- const proxies = Array.isArray(proxySettings) ? proxySettings : [proxySettings];
286
+ const proxy = await (0, utils_1.loadProxyConfiguration)(root, proxyConfig, false);
299
287
  const createdProxies = [];
300
288
  const { createProxyMiddleware } = await Promise.resolve().then(() => __importStar(require('http-proxy-middleware')));
301
- for (const proxy of proxies) {
302
- for (const [key, context] of Object.entries(proxy)) {
303
- if (typeof key === 'string') {
304
- createdProxies.push(createProxyMiddleware(key.replace(/^\*$/, '**').replace(/\/\*$/, ''),
305
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
306
- context));
307
- }
308
- else {
309
- createdProxies.push(
310
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
311
- createProxyMiddleware(key, context));
312
- }
289
+ for (const [key, context] of Object.entries(proxy)) {
290
+ if (typeof key === 'string') {
291
+ createdProxies.push(createProxyMiddleware(key.replace(/^\*$/, '**').replace(/\/\*$/, ''),
292
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
+ context));
294
+ }
295
+ else {
296
+ createdProxies.push(
297
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
298
+ createProxyMiddleware(key, context));
313
299
  }
314
300
  }
315
301
  return createdProxies;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.io/license
7
+ */
8
+ import { BuilderContext } from '@angular-devkit/architect';
9
+ import { Schema as WtrBuilderOptions } from './schema';
10
+ /** Logs a warning for any unsupported options specified. */
11
+ export declare function logBuilderStatusWarnings(options: WtrBuilderOptions, ctx: BuilderContext): void;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ /**
3
+ * @license
4
+ * Copyright Google LLC All Rights Reserved.
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://angular.io/license
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.logBuilderStatusWarnings = void 0;
11
+ const UNSUPPORTED_OPTIONS = [
12
+ 'main',
13
+ 'assets',
14
+ 'scripts',
15
+ 'styles',
16
+ 'inlineStyleLanguage',
17
+ 'stylePreprocessorOptions',
18
+ 'sourceMap',
19
+ 'progress',
20
+ 'poll',
21
+ 'preserveSymlinks',
22
+ 'browsers',
23
+ 'codeCoverage',
24
+ 'codeCoverageExclude',
25
+ 'fileReplacements',
26
+ 'webWorkerTsConfig',
27
+ 'watch',
28
+ ];
29
+ /** Logs a warning for any unsupported options specified. */
30
+ function logBuilderStatusWarnings(options, ctx) {
31
+ // Validate supported options
32
+ for (const unsupportedOption of UNSUPPORTED_OPTIONS) {
33
+ const value = options[unsupportedOption];
34
+ if (value === undefined || value === false) {
35
+ continue;
36
+ }
37
+ if (Array.isArray(value) && value.length === 0) {
38
+ continue;
39
+ }
40
+ if (typeof value === 'object' && Object.keys(value).length === 0) {
41
+ continue;
42
+ }
43
+ ctx.logger.warn(`The '${unsupportedOption}' option is not yet supported by this builder.`);
44
+ }
45
+ }
46
+ exports.logBuilderStatusWarnings = logBuilderStatusWarnings;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.io/license
7
+ */
8
+ import { Schema } from './schema';
9
+ declare const _default: import("../../../../architect/src/internal").Builder<Schema & import("../../../../core/src").JsonObject>;
10
+ export default _default;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ /**
3
+ * @license
4
+ * Copyright Google LLC All Rights Reserved.
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://angular.io/license
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const architect_1 = require("@angular-devkit/architect");
14
+ const node_fs_1 = require("node:fs");
15
+ const node_module_1 = require("node:module");
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ const test_files_1 = require("../../utils/test-files");
18
+ const application_1 = require("../application");
19
+ const schema_1 = require("../browser-esbuild/schema");
20
+ const builder_status_warnings_1 = require("./builder-status-warnings");
21
+ const options_1 = require("./options");
22
+ exports.default = (0, architect_1.createBuilder)(async (schema, ctx) => {
23
+ ctx.logger.warn('NOTE: The Web Test Runner builder is currently EXPERIMENTAL and not ready for production use.');
24
+ (0, builder_status_warnings_1.logBuilderStatusWarnings)(schema, ctx);
25
+ // Dynamic import `@web/test-runner` from the user's workspace. As an optional peer dep, it may not be installed
26
+ // and may not be resolvable from `@angular-devkit/build-angular`.
27
+ const require = (0, node_module_1.createRequire)(`${ctx.workspaceRoot}/`);
28
+ let wtr;
29
+ try {
30
+ wtr = require('@web/test-runner');
31
+ }
32
+ catch {
33
+ return {
34
+ success: false,
35
+ // TODO(dgp1130): Display a more accurate message for non-NPM users.
36
+ error: 'Web Test Runner is not installed, most likely you need to run `npm install @web/test-runner --save-dev` in your project.',
37
+ };
38
+ }
39
+ const options = (0, options_1.normalizeOptions)(schema);
40
+ const testDir = 'dist/test-out';
41
+ // Parallelize startup work.
42
+ const [testFiles] = await Promise.all([
43
+ // Glob for files to test.
44
+ (0, test_files_1.findTestFiles)(options.include, options.exclude, ctx.workspaceRoot).then((files) => Array.from(files).map((file) => node_path_1.default.relative(process.cwd(), file))),
45
+ // Clean build output path.
46
+ node_fs_1.promises.rm(testDir, { recursive: true, force: true }),
47
+ ]);
48
+ // Build the tests and abort on any build failure.
49
+ const buildOutput = await buildTests(testFiles, testDir, options, ctx);
50
+ if (!buildOutput.success) {
51
+ return buildOutput;
52
+ }
53
+ // Run the built tests.
54
+ return await runTests(wtr, `${testDir}/browser`, options);
55
+ });
56
+ /** Build all the given test files and write the result to the given output path. */
57
+ async function buildTests(testFiles, outputPath, options, ctx) {
58
+ const entryPoints = new Set([
59
+ ...testFiles,
60
+ 'jasmine-core/lib/jasmine-core/jasmine.js',
61
+ '@angular-devkit/build-angular/src/builders/web-test-runner/jasmine_runner.js',
62
+ ]);
63
+ // Extract `zone.js/testing` to a separate entry point because it needs to be loaded after Jasmine.
64
+ const [polyfills, hasZoneTesting] = extractZoneTesting(options.polyfills);
65
+ if (hasZoneTesting) {
66
+ entryPoints.add('zone.js/testing');
67
+ }
68
+ // Build tests with `application` builder, using test files as entry points.
69
+ // Also bundle in Jasmine and the Jasmine runner script, which need to share chunked dependencies.
70
+ const buildOutput = await first((0, application_1.buildApplicationInternal)({
71
+ entryPoints,
72
+ tsConfig: options.tsConfig,
73
+ outputPath,
74
+ aot: false,
75
+ index: false,
76
+ outputHashing: schema_1.OutputHashing.None,
77
+ optimization: false,
78
+ externalDependencies: [
79
+ // Resolved by `@web/test-runner` at runtime with dynamically generated code.
80
+ '@web/test-runner-core',
81
+ ],
82
+ sourceMap: {
83
+ scripts: true,
84
+ styles: true,
85
+ vendor: true,
86
+ },
87
+ polyfills,
88
+ }, ctx));
89
+ return buildOutput;
90
+ }
91
+ function extractZoneTesting(polyfills) {
92
+ const polyfillsWithoutZoneTesting = polyfills.filter((polyfill) => polyfill !== 'zone.js/testing');
93
+ const hasZoneTesting = polyfills.length !== polyfillsWithoutZoneTesting.length;
94
+ return [polyfillsWithoutZoneTesting, hasZoneTesting];
95
+ }
96
+ /** Run Web Test Runner on the given directory of bundled JavaScript tests. */
97
+ async function runTests(wtr, testDir, options) {
98
+ const testPagePath = node_path_1.default.resolve(__dirname, 'test_page.html');
99
+ const testPage = await node_fs_1.promises.readFile(testPagePath, 'utf8');
100
+ const runner = await wtr.startTestRunner({
101
+ config: {
102
+ rootDir: testDir,
103
+ files: [
104
+ `${testDir}/**/*.js`,
105
+ `!${testDir}/polyfills.js`,
106
+ `!${testDir}/chunk-*.js`,
107
+ `!${testDir}/jasmine.js`,
108
+ `!${testDir}/jasmine_runner.js`,
109
+ `!${testDir}/testing.js`, // `zone.js/testing`
110
+ ],
111
+ testFramework: {
112
+ config: {
113
+ defaultTimeoutInterval: 5000,
114
+ },
115
+ },
116
+ nodeResolve: true,
117
+ port: 9876,
118
+ watch: options.watch ?? false,
119
+ testRunnerHtml: (_testFramework, _config) => testPage,
120
+ },
121
+ readCliArgs: false,
122
+ readFileConfig: false,
123
+ autoExitProcess: false,
124
+ });
125
+ if (!runner) {
126
+ throw new Error('Failed to start Web Test Runner.');
127
+ }
128
+ // Wait for the tests to complete and stop the runner.
129
+ const passed = (await once(runner, 'finished'));
130
+ await runner.stop();
131
+ // No need to return error messages because Web Test Runner already printed them to the console.
132
+ return { success: passed };
133
+ }
134
+ /** Returns the first item yielded by the given generator and cancels the execution. */
135
+ async function first(generator) {
136
+ for await (const value of generator) {
137
+ return value;
138
+ }
139
+ throw new Error('Expected generator to emit at least once.');
140
+ }
141
+ /** Listens for a single emission of an event and returns the value emitted. */
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ function once(emitter, event) {
144
+ return new Promise((resolve) => {
145
+ const onEmit = (arg) => {
146
+ emitter.off(event, onEmit);
147
+ resolve(arg);
148
+ };
149
+ emitter.on(event, onEmit);
150
+ });
151
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.io/license
7
+ */
8
+
9
+ import { getTestBed } from '@angular/core/testing';
10
+ import {
11
+ BrowserDynamicTestingModule,
12
+ platformBrowserDynamicTesting,
13
+ } from '@angular/platform-browser-dynamic/testing';
14
+ import {
15
+ getConfig,
16
+ sessionFailed,
17
+ sessionFinished,
18
+ sessionStarted,
19
+ } from '@web/test-runner-core/browser/session.js';
20
+
21
+ /** Executes Angular Jasmine tests in the given environment and reports the results to Web Test Runner. */
22
+ export async function runJasmineTests(jasmineEnv) {
23
+ const allSpecs = [];
24
+ const failedSpecs = [];
25
+
26
+ jasmineEnv.addReporter({
27
+ specDone(result) {
28
+ const expectations = [...result.passedExpectations, ...result.failedExpectations];
29
+ allSpecs.push(...expectations.map((e) => ({ name: e.fullName, passed: e.passed })));
30
+
31
+ for (const e of result.failedExpectations) {
32
+ const message = `${result.fullName}\n${e.message}\n${e.stack}`;
33
+ // eslint-disable-next-line no-console
34
+ console.error(message);
35
+ failedSpecs.push({
36
+ message,
37
+ name: e.fullName,
38
+ stack: e.stack,
39
+ expected: e.expected,
40
+ actual: e.actual,
41
+ });
42
+ }
43
+ },
44
+
45
+ async jasmineDone(result) {
46
+ // eslint-disable-next-line no-console
47
+ console.log(`Tests ${result.overallStatus}!`);
48
+ await sessionFinished({
49
+ passed: result.overallStatus === 'passed',
50
+ errors: failedSpecs,
51
+ testResults: {
52
+ name: '',
53
+ suites: [],
54
+ tests: allSpecs,
55
+ },
56
+ });
57
+ },
58
+ });
59
+
60
+ await sessionStarted();
61
+
62
+ // Web Test Runner uses a different HTML page for every test, so we only get one `testFile` for the single `*.js` file we need to execute.
63
+ const { testFile, testFrameworkConfig } = await getConfig();
64
+ const config = { defaultTimeoutInterval: 60_000, ...(testFrameworkConfig ?? {}) };
65
+
66
+ // eslint-disable-next-line no-undef
67
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = config.defaultTimeoutInterval;
68
+
69
+ // Initialize `TestBed` automatically for users. This assumes we already evaluated `zone.js/testing`.
70
+ getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
71
+ errorOnUnknownElements: true,
72
+ errorOnUnknownProperties: true,
73
+ });
74
+
75
+ // Load the test file and evaluate it.
76
+ try {
77
+ // eslint-disable-next-line no-undef
78
+ await import(new URL(testFile, document.baseURI).href);
79
+
80
+ // Execute the test functions.
81
+ // eslint-disable-next-line no-undef
82
+ jasmineEnv.execute();
83
+ } catch (err) {
84
+ // eslint-disable-next-line no-console
85
+ console.error(err);
86
+ await sessionFailed(err);
87
+ }
88
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.io/license
7
+ */
8
+ import { Schema as WtrBuilderSchema } from './schema';
9
+ /**
10
+ * Options supported for the Web Test Runner builder. The schema is an approximate
11
+ * representation of the options type, but this is a more precise version.
12
+ */
13
+ export type WtrBuilderOptions = Overwrite<WtrBuilderSchema, {
14
+ include: string[];
15
+ exclude: string[];
16
+ polyfills: string[];
17
+ }>;
18
+ type Overwrite<Obj extends {}, Overrides extends {}> = Omit<Obj, keyof Overrides> & Overrides;
19
+ /**
20
+ * Normalizes input options validated by the schema to a more precise and useful
21
+ * options type in {@link WtrBuilderOptions}.
22
+ */
23
+ export declare function normalizeOptions(schema: WtrBuilderSchema): WtrBuilderOptions;
24
+ export {};
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * @license
4
+ * Copyright Google LLC All Rights Reserved.
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://angular.io/license
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.normalizeOptions = void 0;
11
+ /**
12
+ * Normalizes input options validated by the schema to a more precise and useful
13
+ * options type in {@link WtrBuilderOptions}.
14
+ */
15
+ function normalizeOptions(schema) {
16
+ return {
17
+ ...schema,
18
+ // Options with default values can't actually be null, even if the types say so.
19
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
20
+ include: schema.include,
21
+ exclude: schema.exclude,
22
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
23
+ polyfills: typeof schema.polyfills === 'string' ? [schema.polyfills] : schema.polyfills ?? [],
24
+ };
25
+ }
26
+ exports.normalizeOptions = normalizeOptions;