diff --git a/LICENSE b/LICENSE index 8876c32c1..48adc1eb1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2017 Google, Inc. +Copyright (c) 2010-2025 Google LLC. https://angular.dev/license Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 99d5d3143..d81d40618 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,27 @@ npm install git+https://github.com/angular/angular-devkit-build-angular-builds.g ``` ---- -# Angular Webpack Build Facade +# @angular-devkit/build-angular -WIP \ No newline at end of file +This package contains [Architect builders](/packages/angular_devkit/architect/README.md) used to build and test Angular applications and libraries. + +## Builders + +| Name | Description | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| application | Build an Angular application targeting a browser and server environment using [esbuild](https://esbuild.github.io). | +| app-shell | Build an Angular [App shell](https://angular.dev/ecosystem/service-workers/app-shell). | +| browser | Build an Angular application targeting a browser environment using [Webpack](https://webpack.js.org). | +| browser-esbuild | Build an Angular application targeting a browser environment using [esbuild](https://esbuild.github.io). | +| dev-server | A development server that provides live reloading. | +| extract-i18n | Extract i18n messages from an Angular application. | +| karma | Execute unit tests using [Karma](https://github.com/karma-runner/karma) test runner. | +| ng-packagr | Build and package an Angular library in [Angular Package Format (APF)](https://angular.dev/tools/libraries/angular-package-format) format using [ng-packagr](https://github.com/ng-packagr/ng-packagr). | +| prerender | [Prerender](https://angular.dev/guide/prerendering) pages of your application. Prerendering is the process where a dynamic page is processed at build time generating static HTML. | +| server | Build an Angular application targeting a [Node.js](https://nodejs.org) environment. | +| ssr-dev-server | A development server which offers live reload during development, but uses server-side rendering. | +| protractor | **Deprecated** - Run end-to-end tests using [Protractor](https://www.protractortest.org/) framework. | + +## Disclaimer + +While the builders when executed via the Angular CLI and their associated options are considered stable, the programmatic APIs are not considered officially supported and are not subject to the breaking change guarantees of SemVer. diff --git a/builders.json b/builders.json index a378a2fd2..aba9dd1ff 100644 --- a/builders.json +++ b/builders.json @@ -1,45 +1,76 @@ { "$schema": "../architect/src/builders-schema.json", "builders": { + "application": "@angular/build:application", "app-shell": { - "implementation": "./src/app-shell", - "schema": "./src/app-shell/schema.json", - "description": "Build a server app and a browser app, then render the index.html and use it for the browser output." + "implementation": "./src/builders/app-shell", + "schema": "./src/builders/app-shell/schema.json", + "description": "Build a server application and a browser application, then render the index.html and use it for the browser output." }, "browser": { - "implementation": "./src/browser", - "schema": "./src/browser/schema.json", - "description": "Build a browser app." + "implementation": "./src/builders/browser", + "schema": "./src/builders/browser/schema.json", + "description": "Build a browser application." + }, + "browser-esbuild": { + "implementation": "./src/builders/browser-esbuild", + "schema": "./src/builders/browser-esbuild/schema.json", + "description": "Build a browser application." }, "dev-server": { - "implementation": "./src/dev-server", - "schema": "./src/dev-server/schema.json", - "description": "Serve a browser app." + "implementation": "./src/builders/dev-server", + "schema": "./src/builders/dev-server/schema.json", + "description": "Serve a browser application." }, "extract-i18n": { - "implementation": "./src/extract-i18n", - "schema": "./src/extract-i18n/schema.json", - "description": "Extract i18n strings from a browser app." + "implementation": "./src/builders/extract-i18n", + "schema": "./src/builders/extract-i18n/schema.json", + "description": "Extract i18n strings from a browser application." + }, + "jest": { + "implementation": "./src/builders/jest", + "schema": "./src/builders/jest/schema.json", + "description": "Run unit tests using Jest." }, "karma": { - "implementation": "./src/karma", - "schema": "./src/karma/schema.json", + "implementation": "./src/builders/karma", + "schema": "./src/builders/karma/schema.json", "description": "Run Karma unit tests." }, + "web-test-runner": { + "implementation": "./src/builders/web-test-runner", + "schema": "./src/builders/web-test-runner/schema.json", + "description": "Run unit tests with Web Test Runner." + }, "protractor": { - "implementation": "./src/protractor", - "schema": "./src/protractor/schema.json", - "description": "Run protractor over a dev server." + "implementation": "./src/builders/protractor-error", + "schema": "./src/builders/protractor/schema.json", + "description": "Throw an error that Protractor is end-of-life and no longer supported." }, - "tslint": { - "implementation": "./src/tslint", - "schema": "./src/tslint/schema.json", - "description": "Run tslint over a TS project." + "private-protractor": { + "implementation": "./src/builders/protractor", + "schema": "./src/builders/protractor/schema.json", + "description": "PRIVATE API - Do not use." }, "server": { - "implementation": "./src/server", - "schema": "./src/server/schema.json", + "implementation": "./src/builders/server", + "schema": "./src/builders/server/schema.json", "description": "Build a server Angular application." + }, + "ng-packagr": { + "implementation": "./src/builders/ng-packagr", + "schema": "./src/builders/ng-packagr/schema.json", + "description": "Build a library with ng-packagr." + }, + "ssr-dev-server": { + "implementation": "./src/builders/ssr-dev-server", + "schema": "./src/builders/ssr-dev-server/schema.json", + "description": "Serve a universal application." + }, + "prerender": { + "implementation": "./src/builders/prerender", + "schema": "./src/builders/prerender/schema.json", + "description": "Perform build-time prerendering of chosen routes." } } } diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 000000000..9671938cf --- /dev/null +++ b/index.d.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export * from './src/index'; diff --git a/index.js b/index.js new file mode 100644 index 000000000..fa26cc105 --- /dev/null +++ b/index.js @@ -0,0 +1,24 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./src/index"), exports); diff --git a/package.json b/package.json index f0a38b709..ba9ef8143 100644 --- a/package.json +++ b/package.json @@ -1,101 +1,146 @@ { "name": "@angular-devkit/build-angular", - "version": "0.1000.0-rc.0+47.5e7e48b", + "version": "21.0.0-next.6+sha-2d41699", "description": "Angular Webpack Build Facade", - "experimental": true, "main": "src/index.js", "typings": "src/index.d.ts", "builders": "builders.json", "dependencies": { - "@angular-devkit/architect": "github:angular/angular-devkit-architect-builds#5e7e48bb9", - "@angular-devkit/build-optimizer": "github:angular/angular-devkit-build-optimizer-builds#5e7e48bb9", - "@angular-devkit/build-webpack": "github:angular/angular-devkit-build-webpack-builds#5e7e48bb9", - "@angular-devkit/core": "github:angular/angular-devkit-core-builds#5e7e48bb9", - "@babel/core": "7.10.2", - "@babel/generator": "7.10.2", - "@babel/plugin-transform-runtime": "7.10.1", - "@babel/preset-env": "7.10.2", - "@babel/runtime": "7.10.2", - "@babel/template": "7.10.1", - "@jsdevtools/coverage-istanbul-loader": "3.0.3", - "@ngtools/webpack": "github:angular/ngtools-webpack-builds#5e7e48bb9", - "ajv": "6.12.2", - "autoprefixer": "9.8.0", - "babel-loader": "8.1.0", - "browserslist": "^4.9.1", - "cacache": "15.0.4", - "caniuse-lite": "^1.0.30001032", - "circular-dependency-plugin": "5.2.0", - "copy-webpack-plugin": "6.0.2", - "core-js": "3.6.4", - "css-loader": "3.5.3", - "cssnano": "4.1.10", - "file-loader": "6.0.0", - "find-cache-dir": "3.3.1", - "glob": "7.1.6", - "jest-worker": "26.0.0", + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "github:angular/angular-devkit-architect-builds#2d41699", + "@angular-devkit/build-webpack": "github:angular/angular-devkit-build-webpack-builds#2d41699", + "@angular-devkit/core": "github:angular/angular-devkit-core-builds#2d41699", + "@angular/build": "github:angular/angular-build-builds#2d41699", + "@babel/core": "7.28.4", + "@babel/generator": "7.28.3", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.28.0", + "@babel/plugin-transform-async-to-generator": "7.27.1", + "@babel/plugin-transform-runtime": "7.28.3", + "@babel/preset-env": "7.28.3", + "@babel/runtime": "7.28.4", + "@discoveryjs/json-ext": "0.6.3", + "@ngtools/webpack": "github:angular/ngtools-webpack-builds#2d41699", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.21", + "babel-loader": "10.0.0", + "browserslist": "^4.26.0", + "copy-webpack-plugin": "13.0.1", + "css-loader": "7.1.2", + "esbuild-wasm": "0.25.10", + "http-proxy-middleware": "3.0.5", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", "karma-source-map-support": "1.4.0", - "less-loader": "6.1.0", - "license-webpack-plugin": "2.2.0", - "loader-utils": "2.0.0", - "mini-css-extract-plugin": "0.9.0", - "minimatch": "3.0.4", - "open": "7.0.4", - "parse5": "4.0.0", - "pnp-webpack-plugin": "1.6.4", - "postcss": "7.0.32", - "postcss-import": "12.0.1", - "postcss-loader": "3.0.0", - "raw-loader": "4.0.1", - "regenerator-runtime": "0.13.5", - "resolve-url-loader": "3.1.1", - "rimraf": "3.0.2", - "rollup": "2.15.0", - "rxjs": "6.5.5", - "sass": "1.26.8", - "sass-loader": "8.0.2", - "semver": "7.3.2", - "source-map": "0.7.3", - "source-map-loader": "1.0.0", - "source-map-support": "0.5.19", - "speed-measure-webpack-plugin": "1.3.3", - "style-loader": "1.2.1", - "stylus": "0.54.7", - "stylus-loader": "3.0.2", - "terser": "4.7.0", - "terser-webpack-plugin": "3.0.3", + "less": "4.4.1", + "less-loader": "12.3.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "mini-css-extract-plugin": "2.9.4", + "open": "10.2.0", + "ora": "9.0.0", + "picomatch": "4.0.3", + "piscina": "5.1.3", + "postcss": "8.5.6", + "postcss-loader": "8.2.0", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.2", + "sass": "1.93.2", + "sass-loader": "16.0.5", + "semver": "7.7.2", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.44.0", + "tinyglobby": "0.2.15", "tree-kill": "1.2.2", - "webpack": "4.43.0", - "webpack-dev-middleware": "3.7.2", - "webpack-dev-server": "3.11.0", - "webpack-merge": "4.2.2", - "webpack-sources": "1.4.3", - "webpack-subresource-integrity": "1.4.1", - "worker-plugin": "4.0.3" + "tslib": "2.8.1", + "webpack": "5.102.0", + "webpack-dev-middleware": "7.4.5", + "webpack-dev-server": "5.2.2", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "optionalDependencies": { + "esbuild": "0.25.10" }, "peerDependencies": { - "@angular/compiler-cli": ">=10.0.0-next.0 < 11", - "typescript": ">=3.9 < 3.10" + "@angular/core": "^21.0.0-next.0", + "@angular/compiler-cli": "^21.0.0-next.0", + "@angular/localize": "^21.0.0-next.0", + "@angular/platform-browser": "^21.0.0-next.0", + "@angular/platform-server": "^21.0.0-next.0", + "@angular/service-worker": "^21.0.0-next.0", + "@angular/ssr": "github:angular/angular-ssr-builds#2d41699", + "@web/test-runner": "^0.20.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^21.0.0-next.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.9 <6.0" }, "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, "@angular/localize": { "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true } }, "keywords": [ - "angular", "Angular CLI", + "Angular DevKit", + "angular", "devkit", - "sdk", - "Angular DevKit" + "sdk" ], "repository": { "type": "git", "url": "https://github.com/angular/angular-cli.git" }, + "packageManager": "pnpm@10.18.0", "engines": { - "node": ">= 10.13.0", - "npm": ">= 6.11.0", + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "author": "Angular Authors", @@ -103,10 +148,5 @@ "bugs": { "url": "https://github.com/angular/angular-cli/issues" }, - "homepage": "https://github.com/angular/angular-cli", - "husky": { - "hooks": { - "pre-push": "node ./bin/devkit-admin hooks/pre-push" - } - } + "homepage": "https://github.com/angular/angular-cli" } diff --git a/plugins/karma.d.ts b/plugins/karma.d.ts index c91a05e54..085a00002 100644 --- a/plugins/karma.d.ts +++ b/plugins/karma.d.ts @@ -1,7 +1,7 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ diff --git a/plugins/karma.js b/plugins/karma.js index d1e2068f3..daee9fe64 100644 --- a/plugins/karma.js +++ b/plugins/karma.js @@ -1,8 +1,9 @@ +"use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -module.exports = require('../src/angular-cli-files/plugins/karma'); +module.exports = require('../src/tools/webpack/plugins/karma/karma'); diff --git a/plugins/webpack/analytics.d.ts b/plugins/webpack/analytics.d.ts deleted file mode 100644 index d6c18bf73..000000000 --- a/plugins/webpack/analytics.d.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { analytics } from '@angular-devkit/core'; -import { Compiler, Module, Stats, compilation } from 'webpack'; -import { Source } from 'webpack-sources'; -declare const NormalModule: any; -interface NormalModule extends Module { - _source?: Source | null; - resource?: string; -} -/** - * Faster than using a RegExp, so we use this to count occurences in source code. - * @param source The source to look into. - * @param match The match string to look for. - * @param wordBreak Whether to check for word break before and after a match was found. - * @return The number of matches found. - * @private - */ -export declare function countOccurrences(source: string, match: string, wordBreak?: boolean): number; -/** - * Holder of statistics related to the build. - */ -declare class AnalyticsBuildStats { - errors: string[]; - numberOfNgOnInit: number; - numberOfComponents: number; - initialChunkSize: number; - totalChunkCount: number; - totalChunkSize: number; - lazyChunkCount: number; - lazyChunkSize: number; - assetCount: number; - assetSize: number; - polyfillSize: number; - cssSize: number; -} -/** - * Analytics plugin that reports the analytics we want from the CLI. - */ -export declare class NgBuildAnalyticsPlugin { - protected _projectRoot: string; - protected _analytics: analytics.Analytics; - protected _category: string; - private _isIvy; - protected _built: boolean; - protected _stats: AnalyticsBuildStats; - constructor(_projectRoot: string, _analytics: analytics.Analytics, _category: string, _isIvy: boolean); - protected _reset(): void; - protected _getMetrics(stats: Stats): (string | number)[]; - protected _getDimensions(stats: Stats): import("../../../../../dist-schema/packages/angular/cli/commands/config").Value[]; - protected _reportBuildMetrics(stats: Stats): void; - protected _reportRebuildMetrics(stats: Stats): void; - protected _checkTsNormalModule(module: NormalModule): void; - protected _checkNgFactoryNormalModule(module: NormalModule): void; - protected _collectErrors(stats: Stats): void; - protected _collectBundleStats(json: any): void; - /************************************************************************************************ - * The next section is all the different Webpack hooks for this plugin. - */ - /** - * Reports a succeed module. - * @private - */ - protected _succeedModule(mod: Module): void; - protected _compilation(compiler: Compiler, compilation: compilation.Compilation): void; - protected _done(stats: Stats): void; - apply(compiler: Compiler): void; -} -export {}; diff --git a/plugins/webpack/analytics.js b/plugins/webpack/analytics.js deleted file mode 100644 index 9aeb1de2a..000000000 --- a/plugins/webpack/analytics.js +++ /dev/null @@ -1,250 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.NgBuildAnalyticsPlugin = exports.countOccurrences = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const core_1 = require("@angular-devkit/core"); -const NormalModule = require('webpack/lib/NormalModule'); -const webpackAllErrorMessageRe = /^([^(]+)\(\d+,\d\): (.*)$/gm; -const webpackTsErrorMessageRe = /^[^(]+\(\d+,\d\): error (TS\d+):/; -/** - * Faster than using a RegExp, so we use this to count occurences in source code. - * @param source The source to look into. - * @param match The match string to look for. - * @param wordBreak Whether to check for word break before and after a match was found. - * @return The number of matches found. - * @private - */ -function countOccurrences(source, match, wordBreak = false) { - if (match.length == 0) { - return source.length + 1; - } - let count = 0; - // We condition here so branch prediction happens out of the loop, not in it. - if (wordBreak) { - const re = /\w/; - for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) { - if (!(re.test(source[pos - 1] || '') || re.test(source[pos + match.length] || ''))) { - count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH! - } - pos -= match.length; - if (pos < 0) { - break; - } - } - } - else { - for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) { - count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH! - pos -= match.length; - if (pos < 0) { - break; - } - } - } - return count; -} -exports.countOccurrences = countOccurrences; -/** - * Holder of statistics related to the build. - */ -class AnalyticsBuildStats { - constructor() { - this.errors = []; - this.numberOfNgOnInit = 0; - this.numberOfComponents = 0; - this.initialChunkSize = 0; - this.totalChunkCount = 0; - this.totalChunkSize = 0; - this.lazyChunkCount = 0; - this.lazyChunkSize = 0; - this.assetCount = 0; - this.assetSize = 0; - this.polyfillSize = 0; - this.cssSize = 0; - } -} -/** - * Analytics plugin that reports the analytics we want from the CLI. - */ -class NgBuildAnalyticsPlugin { - constructor(_projectRoot, _analytics, _category, _isIvy) { - this._projectRoot = _projectRoot; - this._analytics = _analytics; - this._category = _category; - this._isIvy = _isIvy; - this._built = false; - this._stats = new AnalyticsBuildStats(); - } - _reset() { - this._stats = new AnalyticsBuildStats(); - } - _getMetrics(stats) { - const startTime = +(stats.startTime || 0); - const endTime = +(stats.endTime || 0); - const metrics = []; - metrics[core_1.analytics.NgCliAnalyticsMetrics.BuildTime] = (endTime - startTime); - metrics[core_1.analytics.NgCliAnalyticsMetrics.NgOnInitCount] = this._stats.numberOfNgOnInit; - metrics[core_1.analytics.NgCliAnalyticsMetrics.NgComponentCount] = this._stats.numberOfComponents; - metrics[core_1.analytics.NgCliAnalyticsMetrics.InitialChunkSize] = this._stats.initialChunkSize; - metrics[core_1.analytics.NgCliAnalyticsMetrics.TotalChunkCount] = this._stats.totalChunkCount; - metrics[core_1.analytics.NgCliAnalyticsMetrics.TotalChunkSize] = this._stats.totalChunkSize; - metrics[core_1.analytics.NgCliAnalyticsMetrics.LazyChunkCount] = this._stats.lazyChunkCount; - metrics[core_1.analytics.NgCliAnalyticsMetrics.LazyChunkSize] = this._stats.lazyChunkSize; - metrics[core_1.analytics.NgCliAnalyticsMetrics.AssetCount] = this._stats.assetCount; - metrics[core_1.analytics.NgCliAnalyticsMetrics.AssetSize] = this._stats.assetSize; - metrics[core_1.analytics.NgCliAnalyticsMetrics.PolyfillSize] = this._stats.polyfillSize; - metrics[core_1.analytics.NgCliAnalyticsMetrics.CssSize] = this._stats.cssSize; - return metrics; - } - _getDimensions(stats) { - const dimensions = []; - if (this._stats.errors.length) { - // Adding commas before and after so the regex are easier to define filters. - dimensions[core_1.analytics.NgCliAnalyticsDimensions.BuildErrors] = `,${this._stats.errors.join()},`; - } - dimensions[core_1.analytics.NgCliAnalyticsDimensions.NgIvyEnabled] = this._isIvy; - return dimensions; - } - _reportBuildMetrics(stats) { - const dimensions = this._getDimensions(stats); - const metrics = this._getMetrics(stats); - this._analytics.event(this._category, 'build', { dimensions, metrics }); - } - _reportRebuildMetrics(stats) { - const dimensions = this._getDimensions(stats); - const metrics = this._getMetrics(stats); - this._analytics.event(this._category, 'rebuild', { dimensions, metrics }); - } - _checkTsNormalModule(module) { - if (module._source) { - // PLEASE REMEMBER: - // We're dealing with ES5 _or_ ES2015 JavaScript at this point (we don't know for sure). - // Just count the ngOnInit occurences. Comments/Strings/calls occurences should be sparse - // so we just consider them within the margin of error. We do break on word break though. - this._stats.numberOfNgOnInit += countOccurrences(module._source.source(), 'ngOnInit', true); - // Count the number of `Component({` strings (case sensitive), which happens in __decorate(). - // This does not include View Engine AOT compilation, we use the ngfactory for it. - this._stats.numberOfComponents += countOccurrences(module._source.source(), 'Component({'); - // For Ivy we just count ɵcmp. - this._stats.numberOfComponents += countOccurrences(module._source.source(), '.ɵcmp', true); - } - } - _checkNgFactoryNormalModule(module) { - if (module._source) { - // PLEASE REMEMBER: - // We're dealing with ES5 _or_ ES2015 JavaScript at this point (we don't know for sure). - // Count the number of `.ɵccf(` strings (case sensitive). They're calls to components - // factories. - this._stats.numberOfComponents += countOccurrences(module._source.source(), '.ɵccf('); - } - } - _collectErrors(stats) { - if (stats.hasErrors()) { - for (const errObject of stats.compilation.errors) { - if (errObject instanceof Error) { - const allErrors = errObject.message.match(webpackAllErrorMessageRe); - for (const err of [...allErrors || []].slice(1)) { - const message = (err.match(webpackTsErrorMessageRe) || [])[1]; - if (message) { - // At this point this should be a TS1234. - this._stats.errors.push(message); - } - } - } - } - } - } - // We can safely disable no any here since we know the format of the JSON output from webpack. - // tslint:disable-next-line:no-any - _collectBundleStats(json) { - json.chunks - .filter((chunk) => chunk.rendered) - .forEach((chunk) => { - const asset = json.assets.find((x) => x.name == chunk.files[0]); - const size = asset ? asset.size : 0; - if (chunk.entry || chunk.initial) { - this._stats.initialChunkSize += size; - } - else { - this._stats.lazyChunkCount++; - this._stats.lazyChunkSize += size; - } - this._stats.totalChunkCount++; - this._stats.totalChunkSize += size; - }); - json.assets - // Filter out chunks. We only count assets that are not JS. - .filter((a) => { - return json.chunks.every((chunk) => chunk.files[0] != a.name); - }) - .forEach((a) => { - this._stats.assetSize += (a.size || 0); - this._stats.assetCount++; - }); - for (const asset of json.assets) { - if (asset.name == 'polyfill') { - this._stats.polyfillSize += asset.size || 0; - } - } - for (const chunk of json.chunks) { - if (chunk.files[0] && chunk.files[0].endsWith('.css')) { - this._stats.cssSize += chunk.size || 0; - } - } - } - /************************************************************************************************ - * The next section is all the different Webpack hooks for this plugin. - */ - /** - * Reports a succeed module. - * @private - */ - _succeedModule(mod) { - // Only report NormalModule instances. - if (mod.constructor !== NormalModule) { - return; - } - const module = mod; - // Only reports modules that are part of the user's project. We also don't do node_modules. - // There is a chance that someone name a file path `hello_node_modules` or something and we - // will ignore that file for the purpose of gathering, but we're willing to take the risk. - if (!module.resource - || !module.resource.startsWith(this._projectRoot) - || module.resource.indexOf('node_modules') >= 0) { - return; - } - // Check that it's a source file from the project. - if (module.resource.endsWith('.ts')) { - this._checkTsNormalModule(module); - } - else if (module.resource.endsWith('.ngfactory.js')) { - this._checkNgFactoryNormalModule(module); - } - } - _compilation(compiler, compilation) { - this._reset(); - compilation.hooks.succeedModule.tap('NgBuildAnalyticsPlugin', this._succeedModule.bind(this)); - } - _done(stats) { - this._collectErrors(stats); - this._collectBundleStats(stats.toJson()); - if (this._built) { - this._reportRebuildMetrics(stats); - } - else { - this._reportBuildMetrics(stats); - this._built = true; - } - } - apply(compiler) { - compiler.hooks.compilation.tap('NgBuildAnalyticsPlugin', this._compilation.bind(this, compiler)); - compiler.hooks.done.tap('NgBuildAnalyticsPlugin', this._done.bind(this)); - } -} -exports.NgBuildAnalyticsPlugin = NgBuildAnalyticsPlugin; diff --git a/src/angular-cli-files/models/build-options.d.ts b/src/angular-cli-files/models/build-options.d.ts deleted file mode 100644 index 911f5e609..000000000 --- a/src/angular-cli-files/models/build-options.d.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { logging } from '@angular-devkit/core'; -import { ParsedConfiguration } from '@angular/compiler-cli'; -import { AssetPatternClass, Budget, CrossOrigin, ExtraEntryPoint, I18NMissingTranslation, Localize, OptimizationClass, SourceMapClass } from '../../browser/schema'; -import { NormalizedFileReplacement } from '../../utils/normalize-file-replacements'; -export interface BuildOptions { - optimization: OptimizationClass; - environment?: string; - outputPath: string; - resourcesOutputPath?: string; - aot?: boolean; - sourceMap: SourceMapClass; - vendorChunk?: boolean; - commonChunk?: boolean; - baseHref?: string; - deployUrl?: string; - verbose?: boolean; - progress?: boolean; - /** @deprecated since version 9. Use 'locales' object in the project metadata instead.*/ - i18nFile?: string; - /** @deprecated since version 9. No longer needed as the format will be determined automatically.*/ - i18nFormat?: string; - /** @deprecated since version 9. Use 'localize' instead.*/ - i18nLocale?: string; - localize?: Localize; - i18nMissingTranslation?: I18NMissingTranslation; - extractCss?: boolean; - bundleDependencies?: boolean; - externalDependencies?: string[]; - watch?: boolean; - outputHashing?: string; - poll?: number; - deleteOutputPath?: boolean; - preserveSymlinks?: boolean; - extractLicenses?: boolean; - showCircularDependencies?: boolean; - buildOptimizer?: boolean; - namedChunks?: boolean; - crossOrigin?: CrossOrigin; - subresourceIntegrity?: boolean; - serviceWorker?: boolean; - webWorkerTsConfig?: string; - statsJson: boolean; - forkTypeChecker: boolean; - main: string; - polyfills?: string; - budgets: Budget[]; - assets: AssetPatternClass[]; - scripts: ExtraEntryPoint[]; - styles: ExtraEntryPoint[]; - stylePreprocessorOptions?: { - includePaths: string[]; - }; - /** @deprecated SystemJsNgModuleLoader is deprecated, and this is part of its usage. */ - lazyModules: string[]; - platform?: 'browser' | 'server'; - fileReplacements: NormalizedFileReplacement[]; - /** @deprecated use only for compatibility in 8.x; will be removed in 9.0 */ - rebaseRootRelativeCssUrls?: boolean; - esVersionInFileName?: boolean; - experimentalRollupPass?: boolean; - allowedCommonJsDependencies?: string[]; -} -export interface WebpackTestOptions extends BuildOptions { - codeCoverage?: boolean; - codeCoverageExclude?: string[]; -} -export interface WebpackConfigOptions { - root: string; - logger: logging.Logger; - projectRoot: string; - sourceRoot?: string; - buildOptions: T; - tsConfig: ParsedConfiguration; - tsConfigPath: string; - supportES2015: boolean; - differentialLoadingMode?: boolean; -} diff --git a/src/angular-cli-files/models/es5-jit-polyfills.js b/src/angular-cli-files/models/es5-jit-polyfills.js deleted file mode 100644 index febcbb532..000000000 --- a/src/angular-cli-files/models/es5-jit-polyfills.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import 'core-js/es/reflect'; diff --git a/src/angular-cli-files/models/es5-polyfills.js b/src/angular-cli-files/models/es5-polyfills.js deleted file mode 100644 index 9477b8654..000000000 --- a/src/angular-cli-files/models/es5-polyfills.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -// ES2015 symbol capabilities -import 'core-js/es/symbol'; - -// ES2015 function capabilities -import 'core-js/modules/es.function.bind'; -import 'core-js/modules/es.function.name'; -import 'core-js/modules/es.function.has-instance'; - -// ES2015 object capabilities -import 'core-js/modules/es.object.create'; -import 'core-js/modules/es.object.define-property'; -import 'core-js/modules/es.object.define-properties'; -import 'core-js/modules/es.object.get-own-property-descriptor'; -import 'core-js/modules/es.object.get-prototype-of'; -import 'core-js/modules/es.object.keys'; -import 'core-js/modules/es.object.get-own-property-names'; -import 'core-js/modules/es.object.freeze'; -import 'core-js/modules/es.object.seal'; -import 'core-js/modules/es.object.prevent-extensions'; -import 'core-js/modules/es.object.is-frozen'; -import 'core-js/modules/es.object.is-sealed'; -import 'core-js/modules/es.object.is-extensible'; -import 'core-js/modules/es.object.assign'; -import 'core-js/modules/es.object.is'; -import 'core-js/modules/es.object.set-prototype-of'; -import 'core-js/modules/es.object.to-string'; - -// ES2015 array capabilities -import 'core-js/modules/es.array.concat'; -import 'core-js/modules/es.array.is-array'; -import 'core-js/modules/es.array.from'; -import 'core-js/modules/es.array.of'; -import 'core-js/modules/es.array.join'; -import 'core-js/modules/es.array.slice'; -import 'core-js/modules/es.array.splice'; -import 'core-js/modules/es.array.sort'; -import 'core-js/modules/es.array.for-each'; -import 'core-js/modules/es.array.map'; -import 'core-js/modules/es.array.filter'; -import 'core-js/modules/es.array.some'; -import 'core-js/modules/es.array.every'; -import 'core-js/modules/es.array.reduce'; -import 'core-js/modules/es.array.reduce-right'; -import 'core-js/modules/es.array.index-of'; -import 'core-js/modules/es.array.last-index-of'; -import 'core-js/modules/es.array.copy-within'; -import 'core-js/modules/es.array.fill'; -import 'core-js/modules/es.array.find'; -import 'core-js/modules/es.array.find-index'; -import 'core-js/modules/es.array.iterator'; - -// ES2015 string capabilities -import 'core-js/modules/es.string.from-code-point'; -import 'core-js/modules/es.string.raw'; -import 'core-js/modules/es.string.trim'; -import 'core-js/modules/es.string.iterator'; -import 'core-js/modules/es.string.code-point-at'; -import 'core-js/modules/es.string.ends-with'; -import 'core-js/modules/es.string.includes'; -import 'core-js/modules/es.string.repeat'; -import 'core-js/modules/es.string.starts-with'; -import 'core-js/modules/es.string.anchor'; -import 'core-js/modules/es.string.big'; -import 'core-js/modules/es.string.blink'; -import 'core-js/modules/es.string.bold'; -import 'core-js/modules/es.string.fixed'; -import 'core-js/modules/es.string.fontcolor'; -import 'core-js/modules/es.string.fontsize'; -import 'core-js/modules/es.string.italics'; -import 'core-js/modules/es.string.link'; -import 'core-js/modules/es.string.small'; -import 'core-js/modules/es.string.strike'; -import 'core-js/modules/es.string.sub'; -import 'core-js/modules/es.string.sup'; -import 'core-js/modules/es.string.match'; -import 'core-js/modules/es.string.replace'; -import 'core-js/modules/es.string.search'; -import 'core-js/modules/es.string.split'; - -import 'core-js/modules/es.parse-int'; -import 'core-js/modules/es.parse-float'; - -import 'core-js/es/number'; -import 'core-js/es/math'; -import 'core-js/es/date'; - -import 'core-js/modules/es.regexp.constructor'; -import 'core-js/modules/es.regexp.to-string'; -import 'core-js/modules/es.regexp.flags'; - -import 'core-js/modules/es.map'; -import 'core-js/modules/es.weak-map'; -import 'core-js/modules/es.set'; -import 'core-js/modules/web.dom-collections.for-each'; -import 'core-js/modules/web.dom-collections.iterator'; -import 'core-js/modules/es.promise'; -import 'core-js/modules/es.json.to-string-tag'; - -import 'regenerator-runtime/runtime'; \ No newline at end of file diff --git a/src/angular-cli-files/models/jit-polyfills.js b/src/angular-cli-files/models/jit-polyfills.js deleted file mode 100644 index b57ee9bd4..000000000 --- a/src/angular-cli-files/models/jit-polyfills.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import 'core-js/proposals/reflect-metadata'; diff --git a/src/angular-cli-files/models/webpack-configs/browser.d.ts b/src/angular-cli-files/models/webpack-configs/browser.d.ts deleted file mode 100644 index b30304975..000000000 --- a/src/angular-cli-files/models/webpack-configs/browser.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as webpack from 'webpack'; -import { WebpackConfigOptions } from '../build-options'; -export declare function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configuration; diff --git a/src/angular-cli-files/models/webpack-configs/browser.js b/src/angular-cli-files/models/webpack-configs/browser.js deleted file mode 100644 index 734bbb11b..000000000 --- a/src/angular-cli-files/models/webpack-configs/browser.js +++ /dev/null @@ -1,89 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getBrowserConfig = void 0; -const webpack_1 = require("../../plugins/webpack"); -const utils_1 = require("./utils"); -const SubresourceIntegrityPlugin = require('webpack-subresource-integrity'); -const LicenseWebpackPlugin = require('license-webpack-plugin').LicenseWebpackPlugin; -function getBrowserConfig(wco) { - const { buildOptions } = wco; - const { crossOrigin = 'none', subresourceIntegrity, extractLicenses, vendorChunk, commonChunk, styles, allowedCommonJsDependencies, } = buildOptions; - const extraPlugins = []; - const { styles: stylesSourceMap, scripts: scriptsSourceMap, hidden: hiddenSourceMap, } = buildOptions.sourceMap; - if (subresourceIntegrity) { - extraPlugins.push(new SubresourceIntegrityPlugin({ - hashFuncNames: ['sha384'], - })); - } - if (extractLicenses) { - extraPlugins.push(new LicenseWebpackPlugin({ - stats: { - warnings: false, - errors: false, - }, - perChunkOutput: false, - outputFilename: '3rdpartylicenses.txt', - })); - } - if (scriptsSourceMap || stylesSourceMap) { - extraPlugins.push(utils_1.getSourceMapDevTool(scriptsSourceMap, stylesSourceMap, wco.differentialLoadingMode ? true : hiddenSourceMap)); - } - const globalStylesBundleNames = utils_1.normalizeExtraEntryPoints(styles, 'styles') - .map(style => style.bundleName); - let crossOriginLoading = false; - if (subresourceIntegrity && crossOrigin === 'none') { - crossOriginLoading = 'anonymous'; - } - else if (crossOrigin !== 'none') { - crossOriginLoading = crossOrigin; - } - return { - devtool: false, - resolve: { - mainFields: ['es2015', 'browser', 'module', 'main'], - }, - output: { - crossOriginLoading, - }, - optimization: { - runtimeChunk: 'single', - splitChunks: { - maxAsyncRequests: Infinity, - cacheGroups: { - default: !!commonChunk && { - chunks: 'async', - minChunks: 2, - priority: 10, - }, - common: !!commonChunk && { - name: 'common', - chunks: 'async', - minChunks: 2, - enforce: true, - priority: 5, - }, - vendors: false, - vendor: !!vendorChunk && { - name: 'vendor', - chunks: 'initial', - enforce: true, - test: (module, chunks) => { - const moduleName = module.nameForCondition ? module.nameForCondition() : ''; - return /[\\/]node_modules[\\/]/.test(moduleName) - && !chunks.some(({ name }) => utils_1.isPolyfillsEntry(name) - || globalStylesBundleNames.includes(name)); - }, - }, - }, - }, - }, - plugins: [ - new webpack_1.CommonJsUsageWarnPlugin({ - allowedDepedencies: allowedCommonJsDependencies, - }), - ...extraPlugins, - ], - node: false, - }; -} -exports.getBrowserConfig = getBrowserConfig; diff --git a/src/angular-cli-files/models/webpack-configs/common.d.ts b/src/angular-cli-files/models/webpack-configs/common.d.ts deleted file mode 100644 index 1e695abee..000000000 --- a/src/angular-cli-files/models/webpack-configs/common.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Configuration } from 'webpack'; -import { WebpackConfigOptions } from '../build-options'; -export declare function getCommonConfig(wco: WebpackConfigOptions): Configuration; diff --git a/src/angular-cli-files/models/webpack-configs/common.js b/src/angular-cli-files/models/webpack-configs/common.js deleted file mode 100644 index 05dca2933..000000000 --- a/src/angular-cli-files/models/webpack-configs/common.js +++ /dev/null @@ -1,493 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getCommonConfig = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const build_optimizer_1 = require("@angular-devkit/build-optimizer"); -const core_1 = require("@angular-devkit/core"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const fs_1 = require("fs"); -const path = require("path"); -const typescript_1 = require("typescript"); -const webpack_1 = require("webpack"); -const webpack_sources_1 = require("webpack-sources"); -const utils_1 = require("../../../utils"); -const cache_path_1 = require("../../../utils/cache-path"); -const environment_options_1 = require("../../../utils/environment-options"); -const webpack_2 = require("../../plugins/webpack"); -const find_up_1 = require("../../utilities/find-up"); -const utils_2 = require("./utils"); -const ProgressPlugin = require('webpack/lib/ProgressPlugin'); -const CircularDependencyPlugin = require('circular-dependency-plugin'); -const TerserPlugin = require('terser-webpack-plugin'); -const PnpWebpackPlugin = require('pnp-webpack-plugin'); -// tslint:disable-next-line:no-big-function -function getCommonConfig(wco) { - const { root, projectRoot, buildOptions, tsConfig } = wco; - const { styles: stylesOptimization, scripts: scriptsOptimization } = buildOptions.optimization; - const { styles: stylesSourceMap, scripts: scriptsSourceMap, vendor: vendorSourceMap, } = buildOptions.sourceMap; - const extraPlugins = []; - const extraRules = []; - const entryPoints = {}; - // determine hashing format - const hashFormat = utils_2.getOutputHashFormat(buildOptions.outputHashing || 'none'); - const targetInFileName = utils_2.getEsVersionForFileName(tsConfig.options.target, buildOptions.esVersionInFileName); - if (buildOptions.main) { - const mainPath = path.resolve(root, buildOptions.main); - entryPoints['main'] = [mainPath]; - if (buildOptions.experimentalRollupPass) { - // NOTE: the following are known problems with experimentalRollupPass - // - vendorChunk, commonChunk, namedChunks: these won't work, because by the time webpack - // sees the chunks, the context of where they came from is lost. - // - webWorkerTsConfig: workers must be imported via a root relative path (e.g. - // `app/search/search.worker`) instead of a relative path (`/search.worker`) because - // of the same reason as above. - // - loadChildren string syntax: doesn't work because rollup cannot follow the imports. - // Rollup options, except entry module, which is automatically inferred. - const rollupOptions = {}; - // Add rollup plugins/rules. - extraRules.push({ - test: mainPath, - // Ensure rollup loader executes after other loaders. - enforce: 'post', - use: [{ - loader: webpack_2.WebpackRollupLoader, - options: rollupOptions, - }], - }); - // Rollup bundles will include the dynamic System.import that was inside Angular and webpack - // will emit warnings because it can't resolve it. We just ignore it. - // TODO: maybe use https://webpack.js.org/configuration/stats/#statswarningsfilter instead. - // Ignore all "Critical dependency: the request of a dependency is an expression" warnings. - extraPlugins.push(new webpack_1.ContextReplacementPlugin(/./)); - // Ignore "System.import() is deprecated" warnings for the main file and js files. - // Might still get them if @angular/core gets split into a lazy module. - extraRules.push({ - test: mainPath, - enforce: 'post', - parser: { system: true }, - }); - extraRules.push({ - test: /\.js$/, - enforce: 'post', - parser: { system: true }, - }); - } - } - const differentialLoadingMode = !!wco.differentialLoadingMode; - if (wco.buildOptions.platform !== 'server') { - if (differentialLoadingMode || tsConfig.options.target === typescript_1.ScriptTarget.ES5) { - const buildBrowserFeatures = new utils_1.BuildBrowserFeatures(projectRoot, tsConfig.options.target || typescript_1.ScriptTarget.ES5); - if (buildBrowserFeatures.isEs5SupportNeeded()) { - const polyfillsChunkName = 'polyfills-es5'; - entryPoints[polyfillsChunkName] = [path.join(__dirname, '..', 'es5-polyfills.js')]; - if (differentialLoadingMode) { - // Add zone.js legacy support to the es5 polyfills - // This is a noop execution-wise if zone-evergreen is not used. - entryPoints[polyfillsChunkName].push('zone.js/dist/zone-legacy'); - // Since the chunkFileName option schema does not allow the function overload, add a plugin - // that changes the name of the ES5 polyfills chunk to not include ES2015. - extraPlugins.push({ - apply(compiler) { - compiler.hooks.compilation.tap('build-angular', compilation => { - // Webpack typings do not contain MainTemplate assetPath hook - // The webpack.Compilation assetPath hook is a noop in 4.x so the template must be used - // tslint:disable-next-line: no-any - compilation.mainTemplate.hooks.assetPath.tap('build-angular', (filename, data) => { - const assetName = typeof filename === 'function' ? filename(data) : filename; - const isMap = assetName && assetName.endsWith('.map'); - return data.chunk && data.chunk.name === 'polyfills-es5' - ? `polyfills-es5${hashFormat.chunk}.js${isMap ? '.map' : ''}` - : assetName; - }); - }); - }, - }); - } - if (!buildOptions.aot) { - if (differentialLoadingMode) { - entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'jit-polyfills.js')); - } - entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'es5-jit-polyfills.js')); - } - // If not performing a full differential build the polyfills need to be added to ES5 bundle - if (buildOptions.polyfills) { - entryPoints[polyfillsChunkName].push(path.resolve(root, buildOptions.polyfills)); - } - } - } - if (buildOptions.polyfills) { - entryPoints['polyfills'] = [ - ...(entryPoints['polyfills'] || []), - path.resolve(root, buildOptions.polyfills), - ]; - } - if (!buildOptions.aot) { - entryPoints['polyfills'] = [ - ...(entryPoints['polyfills'] || []), - path.join(__dirname, '..', 'jit-polyfills.js'), - ]; - } - } - if (environment_options_1.profilingEnabled) { - extraPlugins.push(new webpack_1.debug.ProfilingPlugin({ - outputPath: path.resolve(root, 'chrome-profiler-events.json'), - })); - } - // process global scripts - const globalScriptsByBundleName = utils_2.normalizeExtraEntryPoints(buildOptions.scripts, 'scripts').reduce((prev, curr) => { - const { bundleName, inject, input } = curr; - const resolvedPath = path.resolve(root, input); - if (!fs_1.existsSync(resolvedPath)) { - throw new Error(`Script file ${input} does not exist.`); - } - const existingEntry = prev.find(el => el.bundleName === bundleName); - if (existingEntry) { - if (existingEntry.inject && !inject) { - // All entries have to be lazy for the bundle to be lazy. - throw new Error(`The ${bundleName} bundle is mixing injected and non-injected scripts.`); - } - existingEntry.paths.push(resolvedPath); - } - else { - prev.push({ - bundleName, - inject, - paths: [resolvedPath], - }); - } - return prev; - }, []); - if (globalScriptsByBundleName.length > 0) { - // Add a new asset for each entry. - globalScriptsByBundleName.forEach(script => { - // Lazy scripts don't get a hash, otherwise they can't be loaded by name. - const hash = script.inject ? hashFormat.script : ''; - const bundleName = script.bundleName; - extraPlugins.push(new webpack_2.ScriptsWebpackPlugin({ - name: bundleName, - sourceMap: scriptsSourceMap, - filename: `${path.basename(bundleName)}${hash}.js`, - scripts: script.paths, - basePath: projectRoot, - })); - }); - } - // process asset entries - if (buildOptions.assets.length) { - const copyWebpackPluginPatterns = buildOptions.assets.map((asset) => { - // Resolve input paths relative to workspace root and add slash at the end. - // tslint:disable-next-line: prefer-const - let { input, output, ignore = [], glob } = asset; - input = path.resolve(root, input).replace(/\\/g, '/'); - input = input.endsWith('/') ? input : input + '/'; - output = output.endsWith('/') ? output : output + '/'; - if (output.startsWith('..')) { - throw new Error('An asset cannot be written to a location outside of the output path.'); - } - return { - context: input, - // Now we remove starting slash to make Webpack place it from the output root. - to: output.replace(/^\//, ''), - from: glob, - noErrorOnMissing: true, - globOptions: { - dot: true, - ignore: [ - '.gitkeep', - '**/.DS_Store', - '**/Thumbs.db', - // Negate patterns needs to be absolute because copy-webpack-plugin uses absolute globs which - // causes negate patterns not to match. - // See: https://github.com/webpack-contrib/copy-webpack-plugin/issues/498#issuecomment-639327909 - ...ignore, - ].map(i => path.posix.join(input, i)), - }, - }; - }); - extraPlugins.push(new CopyWebpackPlugin({ - patterns: copyWebpackPluginPatterns, - })); - } - if (buildOptions.progress) { - extraPlugins.push(new ProgressPlugin({ profile: buildOptions.verbose })); - } - if (buildOptions.showCircularDependencies) { - extraPlugins.push(new CircularDependencyPlugin({ - exclude: /([\\\/]node_modules[\\\/])|(ngfactory\.js$)/, - })); - } - if (buildOptions.statsJson) { - extraPlugins.push(new (class { - apply(compiler) { - compiler.hooks.emit.tap('angular-cli-stats', compilation => { - const data = JSON.stringify(compilation.getStats().toJson('verbose')); - compilation.assets['stats.json'] = new webpack_sources_1.RawSource(data); - }); - } - })()); - } - if (buildOptions.namedChunks) { - extraPlugins.push(new webpack_2.NamedLazyChunksPlugin()); - } - if (!differentialLoadingMode) { - // Budgets are computed after differential builds, not via a plugin. - // https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_angular/src/browser/index.ts - extraPlugins.push(new webpack_2.BundleBudgetPlugin({ budgets: buildOptions.budgets })); - } - let sourceMapUseRule; - if ((scriptsSourceMap || stylesSourceMap) && vendorSourceMap) { - sourceMapUseRule = { - use: [ - { - loader: require.resolve('source-map-loader'), - }, - ], - }; - } - let buildOptimizerUseRule = []; - if (buildOptions.buildOptimizer) { - extraPlugins.push(new build_optimizer_1.BuildOptimizerWebpackPlugin()); - buildOptimizerUseRule = [ - { - loader: build_optimizer_1.buildOptimizerLoaderPath, - options: { sourceMap: scriptsSourceMap }, - }, - ]; - } - // Allow loaders to be in a node_modules nested inside the devkit/build-angular package. - // This is important in case loaders do not get hoisted. - // If this file moves to another location, alter potentialNodeModules as well. - const loaderNodeModules = find_up_1.findAllNodeModules(__dirname, projectRoot); - loaderNodeModules.unshift('node_modules'); - const extraMinimizers = []; - if (stylesOptimization) { - extraMinimizers.push(new webpack_2.OptimizeCssWebpackPlugin({ - sourceMap: stylesSourceMap, - // component styles retain their original file name - test: file => /\.(?:css|scss|sass|less|styl)$/.test(file), - })); - } - if (scriptsOptimization) { - let angularGlobalDefinitions = { - ngDevMode: false, - ngI18nClosureMode: false, - }; - // Try to load known global definitions from @angular/compiler-cli. - const GLOBAL_DEFS_FOR_TERSER = require('@angular/compiler-cli').GLOBAL_DEFS_FOR_TERSER; - if (GLOBAL_DEFS_FOR_TERSER) { - angularGlobalDefinitions = GLOBAL_DEFS_FOR_TERSER; - } - if (buildOptions.aot) { - // Also try to load AOT-only global definitions. - const GLOBAL_DEFS_FOR_TERSER_WITH_AOT = require('@angular/compiler-cli') - .GLOBAL_DEFS_FOR_TERSER_WITH_AOT; - if (GLOBAL_DEFS_FOR_TERSER_WITH_AOT) { - angularGlobalDefinitions = { - ...angularGlobalDefinitions, - ...GLOBAL_DEFS_FOR_TERSER_WITH_AOT, - }; - } - } - // TODO: Investigate why this fails for some packages: wco.supportES2015 ? 6 : 5; - const terserEcma = 5; - const terserOptions = { - warnings: !!buildOptions.verbose, - safari10: true, - output: { - ecma: terserEcma, - // For differential loading, this is handled in the bundle processing. - // This should also work with just true but the experimental rollup support breaks without this check. - ascii_only: !differentialLoadingMode, - // default behavior (undefined value) is to keep only important comments (licenses, etc.) - comments: !buildOptions.extractLicenses && undefined, - webkit: true, - beautify: environment_options_1.shouldBeautify, - }, - // On server, we don't want to compress anything. We still set the ngDevMode = false for it - // to remove dev code, and ngI18nClosureMode to remove Closure compiler i18n code - compress: environment_options_1.allowMinify && - (buildOptions.platform == 'server' - ? { - ecma: terserEcma, - global_defs: angularGlobalDefinitions, - keep_fnames: true, - } - : { - ecma: terserEcma, - pure_getters: buildOptions.buildOptimizer, - // PURE comments work best with 3 passes. - // See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926. - passes: buildOptions.buildOptimizer ? 3 : 1, - global_defs: angularGlobalDefinitions, - }), - // We also want to avoid mangling on server. - // Name mangling is handled within the browser builder - mangle: environment_options_1.allowMangle && buildOptions.platform !== 'server' && !differentialLoadingMode, - }; - const globalScriptsNames = globalScriptsByBundleName.map(s => s.bundleName); - extraMinimizers.push(new TerserPlugin({ - sourceMap: scriptsSourceMap, - parallel: utils_1.maxWorkers, - cache: !environment_options_1.cachingDisabled && cache_path_1.findCachePath('terser-webpack'), - extractComments: false, - exclude: globalScriptsNames, - terserOptions, - }), - // Script bundles are fully optimized here in one step since they are never downleveled. - // They are shared between ES2015 & ES5 outputs so must support ES5. - new TerserPlugin({ - sourceMap: scriptsSourceMap, - parallel: utils_1.maxWorkers, - cache: !environment_options_1.cachingDisabled && cache_path_1.findCachePath('terser-webpack'), - extractComments: false, - include: globalScriptsNames, - terserOptions: { - ...terserOptions, - compress: environment_options_1.allowMinify && { - ...terserOptions.compress, - ecma: 5, - }, - output: { - ...terserOptions.output, - ecma: 5, - }, - mangle: environment_options_1.allowMangle && buildOptions.platform !== 'server', - }, - })); - } - if (wco.tsConfig.options.target !== undefined && - wco.tsConfig.options.target >= typescript_1.ScriptTarget.ES2017) { - wco.logger.warn(core_1.tags.stripIndent ` - WARNING: Zone.js does not support native async/await in ES2017. - These blocks are not intercepted by zone.js and will not triggering change detection. - See: https://github.com/angular/zone.js/pull/1140 for more information. - `); - } - return { - mode: scriptsOptimization || stylesOptimization ? 'production' : 'development', - devtool: false, - profile: buildOptions.statsJson, - resolve: { - extensions: ['.ts', '.tsx', '.mjs', '.js'], - symlinks: !buildOptions.preserveSymlinks, - modules: [wco.tsConfig.options.baseUrl || projectRoot, 'node_modules'], - plugins: [ - PnpWebpackPlugin, - new webpack_2.DedupeModuleResolvePlugin({ verbose: buildOptions.verbose }), - ], - }, - resolveLoader: { - symlinks: !buildOptions.preserveSymlinks, - modules: loaderNodeModules, - plugins: [PnpWebpackPlugin.moduleLoader(module)], - }, - context: projectRoot, - entry: entryPoints, - output: { - futureEmitAssets: true, - path: path.resolve(root, buildOptions.outputPath), - publicPath: buildOptions.deployUrl, - filename: `[name]${targetInFileName}${hashFormat.chunk}.js`, - }, - watch: buildOptions.watch, - watchOptions: { - poll: buildOptions.poll, - ignored: buildOptions.poll === undefined ? undefined : /[\\\/]node_modules[\\\/]/, - }, - performance: { - hints: false, - }, - module: { - // Show an error for missing exports instead of a warning. - strictExportPresence: true, - rules: [ - { - test: /\.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/, - loader: require.resolve('file-loader'), - options: { - name: `[name]${hashFormat.file}.[ext]`, - // Re-use emitted files from browser builder on the server. - emitFile: wco.buildOptions.platform !== 'server', - }, - }, - { - // Mark files inside `@angular/core` as using SystemJS style dynamic imports. - // Removing this will cause deprecation warnings to appear. - test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, - parser: { system: true }, - }, - { - test: /\.m?js$/, - exclude: [/[\/\\](?:core-js|\@babel|tslib)[\/\\]/, /(ngfactory|ngstyle)\.js$/], - use: [ - ...(wco.supportES2015 - ? [] - : [ - { - loader: require.resolve('babel-loader'), - options: { - babelrc: false, - configFile: false, - compact: false, - cacheCompression: false, - cacheDirectory: cache_path_1.findCachePath('babel-webpack'), - cacheIdentifier: JSON.stringify({ - buildAngular: require('../../../../package.json').version, - }), - presets: [ - [ - require.resolve('@babel/preset-env'), - { - bugfixes: true, - modules: false, - // Comparable behavior to tsconfig target of ES5 - targets: { ie: 9 }, - exclude: ['transform-typeof-symbol'], - }, - ], - ], - plugins: [ - [ - require('@babel/plugin-transform-runtime').default, - { - useESModules: true, - version: require('@babel/runtime/package.json').version, - absoluteRuntime: path.dirname(require.resolve('@babel/runtime/package.json')), - }, - ], - ], - }, - }, - ]), - ...buildOptimizerUseRule, - ], - }, - { - test: /\.m?js$/, - exclude: /(ngfactory|ngstyle)\.js$/, - enforce: 'pre', - ...sourceMapUseRule, - }, - ...extraRules, - ], - }, - optimization: { - noEmitOnErrors: true, - minimizer: [new webpack_1.HashedModuleIdsPlugin(), ...extraMinimizers], - }, - plugins: [ - // Always replace the context for the System.import in angular/core to prevent warnings. - // https://github.com/angular/angular/issues/11580 - // With VE the correct context is added in @ngtools/webpack, but Ivy doesn't need it at all. - new webpack_1.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)/), - ...extraPlugins, - ], - }; -} -exports.getCommonConfig = getCommonConfig; diff --git a/src/angular-cli-files/models/webpack-configs/index.d.ts b/src/angular-cli-files/models/webpack-configs/index.d.ts deleted file mode 100644 index 11bbaeaa3..000000000 --- a/src/angular-cli-files/models/webpack-configs/index.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -export * from './browser'; -export * from './common'; -export * from './server'; -export * from './styles'; -export * from './test'; -export * from './typescript'; -export * from './utils'; -export * from './stats'; -export * from './worker'; diff --git a/src/angular-cli-files/models/webpack-configs/index.js b/src/angular-cli-files/models/webpack-configs/index.js deleted file mode 100644 index 0ceb24a83..000000000 --- a/src/angular-cli-files/models/webpack-configs/index.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -__exportStar(require("./browser"), exports); -__exportStar(require("./common"), exports); -__exportStar(require("./server"), exports); -__exportStar(require("./styles"), exports); -__exportStar(require("./test"), exports); -__exportStar(require("./typescript"), exports); -__exportStar(require("./utils"), exports); -__exportStar(require("./stats"), exports); -__exportStar(require("./worker"), exports); diff --git a/src/angular-cli-files/models/webpack-configs/server.d.ts b/src/angular-cli-files/models/webpack-configs/server.d.ts deleted file mode 100644 index 1ef93809a..000000000 --- a/src/angular-cli-files/models/webpack-configs/server.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Configuration } from 'webpack'; -import { WebpackConfigOptions } from '../build-options'; -/** - * Returns a partial specific to creating a bundle for node - * @param wco Options which are include the build options and app config - */ -export declare function getServerConfig(wco: WebpackConfigOptions): Configuration; diff --git a/src/angular-cli-files/models/webpack-configs/server.js b/src/angular-cli-files/models/webpack-configs/server.js deleted file mode 100644 index f01b18881..000000000 --- a/src/angular-cli-files/models/webpack-configs/server.js +++ /dev/null @@ -1,68 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getServerConfig = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const path_1 = require("path"); -const webpack_1 = require("webpack"); -const utils_1 = require("./utils"); -/** - * Returns a partial specific to creating a bundle for node - * @param wco Options which are include the build options and app config - */ -function getServerConfig(wco) { - const { sourceMap, bundleDependencies, externalDependencies = [], } = wco.buildOptions; - const extraPlugins = []; - if (sourceMap) { - const { scripts, styles, hidden } = sourceMap; - if (scripts || styles) { - extraPlugins.push(utils_1.getSourceMapDevTool(scripts, styles, hidden)); - } - } - const config = { - resolve: { - mainFields: ['es2015', 'main', 'module'], - }, - target: 'node', - output: { - libraryTarget: 'commonjs', - }, - plugins: [ - // Fixes Critical dependency: the request of a dependency is an expression - new webpack_1.ContextReplacementPlugin(/@?hapi(\\|\/)/), - new webpack_1.ContextReplacementPlugin(/express(\\|\/)/), - ...extraPlugins, - ], - node: false, - }; - if (bundleDependencies) { - config.externals = [...externalDependencies]; - } - else { - config.externals = [ - ...externalDependencies, - (context, request, callback) => { - // Absolute & Relative paths are not externals - if (request.startsWith('./') || path_1.isAbsolute(request)) { - callback(); - return; - } - try { - require.resolve(request); - callback(null, request); - } - catch (_a) { - // Node couldn't find it, so it must be user-aliased - callback(); - } - }, - ]; - } - return config; -} -exports.getServerConfig = getServerConfig; diff --git a/src/angular-cli-files/models/webpack-configs/stats.d.ts b/src/angular-cli-files/models/webpack-configs/stats.d.ts deleted file mode 100644 index 831f1b9da..000000000 --- a/src/angular-cli-files/models/webpack-configs/stats.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { WebpackConfigOptions } from '../build-options'; -export declare function getWebpackStatsConfig(verbose?: boolean): { - colors: boolean; - hash: boolean; - timings: boolean; - chunks: boolean; - chunkModules: boolean; - children: boolean; - modules: boolean; - reasons: boolean; - warnings: boolean; - errors: boolean; - assets: boolean; - version: boolean; - errorDetails: boolean; - moduleTrace: boolean; -}; -export declare function getStatsConfig(wco: WebpackConfigOptions): { - stats: { - colors: boolean; - hash: boolean; - timings: boolean; - chunks: boolean; - chunkModules: boolean; - children: boolean; - modules: boolean; - reasons: boolean; - warnings: boolean; - errors: boolean; - assets: boolean; - version: boolean; - errorDetails: boolean; - moduleTrace: boolean; - }; -}; diff --git a/src/angular-cli-files/models/webpack-configs/stats.js b/src/angular-cli-files/models/webpack-configs/stats.js deleted file mode 100644 index 40b59501d..000000000 --- a/src/angular-cli-files/models/webpack-configs/stats.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getStatsConfig = exports.getWebpackStatsConfig = void 0; -const webpackOutputOptions = { - colors: true, - hash: true, - timings: true, - chunks: true, - chunkModules: false, - children: false, - modules: false, - reasons: false, - warnings: true, - errors: true, - assets: true, - version: false, - errorDetails: false, - moduleTrace: false, -}; -const verboseWebpackOutputOptions = { - // The verbose output will most likely be piped to a file, so colors just mess it up. - colors: false, - usedExports: true, - maxModules: Infinity, - optimizationBailout: true, - reasons: true, - children: true, - assets: true, - version: true, - chunkModules: true, - errorDetails: true, - moduleTrace: true, -}; -function getWebpackStatsConfig(verbose = false) { - return verbose - ? Object.assign(webpackOutputOptions, verboseWebpackOutputOptions) - : webpackOutputOptions; -} -exports.getWebpackStatsConfig = getWebpackStatsConfig; -function getStatsConfig(wco) { - const verbose = !!wco.buildOptions.verbose; - return { stats: getWebpackStatsConfig(verbose) }; -} -exports.getStatsConfig = getStatsConfig; diff --git a/src/angular-cli-files/models/webpack-configs/styles.d.ts b/src/angular-cli-files/models/webpack-configs/styles.d.ts deleted file mode 100644 index 02ebabc2c..000000000 --- a/src/angular-cli-files/models/webpack-configs/styles.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as webpack from 'webpack'; -import { WebpackConfigOptions } from '../build-options'; -export declare function getStylesConfig(wco: WebpackConfigOptions): { - entry: { - [key: string]: string[]; - }; - module: { - rules: webpack.RuleSetRule[]; - }; - plugins: webpack.Plugin[]; -}; diff --git a/src/angular-cli-files/models/webpack-configs/styles.js b/src/angular-cli-files/models/webpack-configs/styles.js deleted file mode 100644 index 4083f3726..000000000 --- a/src/angular-cli-files/models/webpack-configs/styles.js +++ /dev/null @@ -1,231 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getStylesConfig = void 0; -const path = require("path"); -const webpack_1 = require("../../plugins/webpack"); -const utils_1 = require("./utils"); -const autoprefixer = require('autoprefixer'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const postcssImports = require('postcss-import'); -// tslint:disable-next-line:no-big-function -function getStylesConfig(wco) { - const { root, buildOptions } = wco; - const entryPoints = {}; - const globalStylePaths = []; - const extraPlugins = [ - new webpack_1.AnyComponentStyleBudgetChecker(buildOptions.budgets), - ]; - const cssSourceMap = buildOptions.sourceMap.styles; - // Determine hashing format. - const hashFormat = utils_1.getOutputHashFormat(buildOptions.outputHashing); - const postcssPluginCreator = function (loader) { - return [ - postcssImports({ - resolve: (url) => (url.startsWith('~') ? url.substr(1) : url), - load: (filename) => { - return new Promise((resolve, reject) => { - loader.fs.readFile(filename, (err, data) => { - if (err) { - reject(err); - return; - } - const content = data.toString(); - resolve(content); - }); - }); - }, - }), - webpack_1.PostcssCliResources({ - baseHref: buildOptions.baseHref, - deployUrl: buildOptions.deployUrl, - resourcesOutputPath: buildOptions.resourcesOutputPath, - loader, - rebaseRootRelative: buildOptions.rebaseRootRelativeCssUrls, - filename: `[name]${hashFormat.file}.[ext]`, - emitFile: buildOptions.platform !== 'server', - }), - autoprefixer(), - ]; - }; - // use includePaths from appConfig - const includePaths = []; - let lessPathOptions = {}; - if (buildOptions.stylePreprocessorOptions && - buildOptions.stylePreprocessorOptions.includePaths && - buildOptions.stylePreprocessorOptions.includePaths.length > 0) { - buildOptions.stylePreprocessorOptions.includePaths.forEach((includePath) => includePaths.push(path.resolve(root, includePath))); - lessPathOptions = { - paths: includePaths, - }; - } - // Process global styles. - if (buildOptions.styles.length > 0) { - const chunkNames = []; - utils_1.normalizeExtraEntryPoints(buildOptions.styles, 'styles').forEach(style => { - const resolvedPath = path.resolve(root, style.input); - // Add style entry points. - if (entryPoints[style.bundleName]) { - entryPoints[style.bundleName].push(resolvedPath); - } - else { - entryPoints[style.bundleName] = [resolvedPath]; - } - // Add non injected styles to the list. - if (!style.inject) { - chunkNames.push(style.bundleName); - } - // Add global css paths. - globalStylePaths.push(resolvedPath); - }); - if (chunkNames.length > 0) { - // Add plugin to remove hashes from lazy styles. - extraPlugins.push(new webpack_1.RemoveHashPlugin({ chunkNames, hashFormat })); - } - } - let sassImplementation; - try { - // tslint:disable-next-line:no-implicit-dependencies - sassImplementation = require('node-sass'); - } - catch (_a) { - sassImplementation = require('sass'); - } - // set base rules to derive final rules from - const baseRules = [ - { test: /\.css$/, use: [] }, - { - test: /\.scss$|\.sass$/, - use: [ - { - loader: require.resolve('resolve-url-loader'), - options: { - sourceMap: cssSourceMap, - }, - }, - { - loader: require.resolve('sass-loader'), - options: { - implementation: sassImplementation, - sourceMap: true, - sassOptions: { - // bootstrap-sass requires a minimum precision of 8 - precision: 8, - includePaths, - // Use expanded as otherwise sass will remove comments that are needed for autoprefixer - // Ex: /* autoprefixer grid: autoplace */ - // tslint:disable-next-line: max-line-length - // See: https://github.com/webpack-contrib/sass-loader/blob/45ad0be17264ceada5f0b4fb87e9357abe85c4ff/src/getSassOptions.js#L68-L70 - outputStyle: 'expanded', - }, - }, - }, - ], - }, - { - test: /\.less$/, - use: [ - { - loader: require.resolve('less-loader'), - options: { - sourceMap: cssSourceMap, - lessOptions: { - javascriptEnabled: true, - ...lessPathOptions, - }, - }, - }, - ], - }, - { - test: /\.styl$/, - use: [ - { - loader: require.resolve('resolve-url-loader'), - options: { - sourceMap: cssSourceMap, - }, - }, - { - loader: require.resolve('stylus-loader'), - options: { - sourceMap: { comment: false }, - paths: includePaths, - }, - }, - ], - }, - ]; - // load component css as raw strings - const rules = baseRules.map(({ test, use }) => ({ - exclude: globalStylePaths, - test, - use: [ - { loader: require.resolve('raw-loader') }, - { - loader: require.resolve('postcss-loader'), - options: { - ident: 'embedded', - plugins: postcssPluginCreator, - sourceMap: cssSourceMap - // Never use component css sourcemap when style optimizations are on. - // It will just increase bundle size without offering good debug experience. - && !buildOptions.optimization.styles - // Inline all sourcemap types except hidden ones, which are the same as no sourcemaps - // for component css. - && !buildOptions.sourceMap.hidden ? 'inline' : false, - }, - }, - ...use, - ], - })); - // load global css as css files - if (globalStylePaths.length > 0) { - rules.push(...baseRules.map(({ test, use }) => { - return { - include: globalStylePaths, - test, - use: [ - buildOptions.extractCss ? MiniCssExtractPlugin.loader : require.resolve('style-loader'), - { - loader: require.resolve('css-loader'), - options: { - url: false, - sourceMap: cssSourceMap, - }, - }, - { - loader: require.resolve('postcss-loader'), - options: { - ident: buildOptions.extractCss ? 'extracted' : 'embedded', - plugins: postcssPluginCreator, - sourceMap: cssSourceMap && !buildOptions.extractCss && !buildOptions.sourceMap.hidden - ? 'inline' - : cssSourceMap, - }, - }, - ...use, - ], - }; - })); - } - if (buildOptions.extractCss) { - extraPlugins.push( - // extract global css from js files into own css file - new MiniCssExtractPlugin({ filename: `[name]${hashFormat.extract}.css` }), - // suppress empty .js files in css only entry points - new webpack_1.SuppressExtractedTextChunksWebpackPlugin()); - } - return { - entry: entryPoints, - module: { rules }, - plugins: extraPlugins, - }; -} -exports.getStylesConfig = getStylesConfig; diff --git a/src/angular-cli-files/models/webpack-configs/test.d.ts b/src/angular-cli-files/models/webpack-configs/test.d.ts deleted file mode 100644 index 14f4070d6..000000000 --- a/src/angular-cli-files/models/webpack-configs/test.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as webpack from 'webpack'; -import { WebpackConfigOptions, WebpackTestOptions } from '../build-options'; -export declare function getTestConfig(wco: WebpackConfigOptions): webpack.Configuration; diff --git a/src/angular-cli-files/models/webpack-configs/test.js b/src/angular-cli-files/models/webpack-configs/test.js deleted file mode 100644 index 5ec48d098..000000000 --- a/src/angular-cli-files/models/webpack-configs/test.js +++ /dev/null @@ -1,79 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getTestConfig = void 0; -const glob = require("glob"); -const path = require("path"); -const utils_1 = require("./utils"); -function getTestConfig(wco) { - const { root, buildOptions, sourceRoot: include } = wco; - const extraRules = []; - const extraPlugins = []; - if (buildOptions.codeCoverage) { - const codeCoverageExclude = buildOptions.codeCoverageExclude; - const exclude = [ - /\.(e2e|spec)\.tsx?$/, - /node_modules/, - ]; - if (codeCoverageExclude) { - codeCoverageExclude.forEach((excludeGlob) => { - const excludeFiles = glob - .sync(path.join(root, excludeGlob), { nodir: true }) - .map(file => path.normalize(file)); - exclude.push(...excludeFiles); - }); - } - extraRules.push({ - test: /\.(jsx?|tsx?)$/, - loader: require.resolve('@jsdevtools/coverage-istanbul-loader'), - options: { esModules: true }, - enforce: 'post', - exclude, - include, - }); - } - if (wco.buildOptions.sourceMap) { - const { styles, scripts } = wco.buildOptions.sourceMap; - if (styles || scripts) { - extraPlugins.push(utils_1.getSourceMapDevTool(scripts, styles, false, true)); - } - } - return { - mode: 'development', - resolve: { - mainFields: ['es2015', 'browser', 'module', 'main'], - }, - devtool: buildOptions.sourceMap ? false : 'eval', - entry: { - main: path.resolve(root, buildOptions.main), - }, - module: { - rules: extraRules, - }, - plugins: extraPlugins, - optimization: { - splitChunks: { - chunks: ((chunk) => !utils_1.isPolyfillsEntry(chunk.name)), - cacheGroups: { - vendors: false, - vendor: { - name: 'vendor', - chunks: 'initial', - test: (module, chunks) => { - const moduleName = module.nameForCondition ? module.nameForCondition() : ''; - return /[\\/]node_modules[\\/]/.test(moduleName) - && !chunks.some(({ name }) => utils_1.isPolyfillsEntry(name)); - }, - }, - }, - }, - }, - }; -} -exports.getTestConfig = getTestConfig; diff --git a/src/angular-cli-files/models/webpack-configs/typescript.d.ts b/src/angular-cli-files/models/webpack-configs/typescript.d.ts deleted file mode 100644 index ea89053a7..000000000 --- a/src/angular-cli-files/models/webpack-configs/typescript.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { AngularCompilerPlugin } from '@ngtools/webpack'; -import { WebpackConfigOptions } from '../build-options'; -export declare function getNonAotConfig(wco: WebpackConfigOptions): { - module: { - rules: { - test: RegExp; - loader: string; - }[]; - }; - plugins: AngularCompilerPlugin[]; -}; -export declare function getAotConfig(wco: WebpackConfigOptions, i18nExtract?: boolean): { - module: { - rules: { - test: RegExp; - use: any[]; - }[]; - }; - plugins: AngularCompilerPlugin[]; -}; -export declare function getTypescriptWorkerPlugin(wco: WebpackConfigOptions, workerTsConfigPath: string): AngularCompilerPlugin; diff --git a/src/angular-cli-files/models/webpack-configs/typescript.js b/src/angular-cli-files/models/webpack-configs/typescript.js deleted file mode 100644 index 895315ab5..000000000 --- a/src/angular-cli-files/models/webpack-configs/typescript.js +++ /dev/null @@ -1,124 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getTypescriptWorkerPlugin = exports.getAotConfig = exports.getNonAotConfig = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. -const build_optimizer_1 = require("@angular-devkit/build-optimizer"); -const path = require("path"); -const webpack_1 = require("@ngtools/webpack"); -function _pluginOptionsOverrides(buildOptions, pluginOptions) { - const compilerOptions = { - ...(pluginOptions.compilerOptions || {}) - }; - const hostReplacementPaths = {}; - if (buildOptions.fileReplacements) { - for (const replacement of buildOptions.fileReplacements) { - hostReplacementPaths[replacement.replace] = replacement.with; - } - } - if (buildOptions.preserveSymlinks) { - compilerOptions.preserveSymlinks = true; - } - return { - ...pluginOptions, - hostReplacementPaths, - compilerOptions - }; -} -function _createAotPlugin(wco, options, i18nExtract = false) { - const { root, buildOptions } = wco; - const i18nInFile = buildOptions.i18nFile - ? path.resolve(root, buildOptions.i18nFile) - : undefined; - const i18nFileAndFormat = i18nExtract - ? { - i18nOutFile: buildOptions.i18nFile, - i18nOutFormat: buildOptions.i18nFormat, - } : { - i18nInFile: i18nInFile, - i18nInFormat: buildOptions.i18nFormat, - }; - const compilerOptions = options.compilerOptions || {}; - if (i18nExtract) { - // Extraction of i18n is still using the legacy VE pipeline - compilerOptions.enableIvy = false; - } - const additionalLazyModules = {}; - if (buildOptions.lazyModules) { - for (const lazyModule of buildOptions.lazyModules) { - additionalLazyModules[lazyModule] = path.resolve(root, lazyModule); - } - } - let pluginOptions = { - mainPath: path.join(root, buildOptions.main), - ...i18nFileAndFormat, - locale: buildOptions.i18nLocale, - platform: buildOptions.platform === 'server' ? webpack_1.PLATFORM.Server : webpack_1.PLATFORM.Browser, - missingTranslation: buildOptions.i18nMissingTranslation, - sourceMap: buildOptions.sourceMap.scripts, - additionalLazyModules, - nameLazyFiles: buildOptions.namedChunks, - forkTypeChecker: buildOptions.forkTypeChecker, - contextElementDependencyConstructor: require('webpack/lib/dependencies/ContextElementDependency'), - logger: wco.logger, - directTemplateLoading: true, - ...options, - compilerOptions, - }; - pluginOptions = _pluginOptionsOverrides(buildOptions, pluginOptions); - return new webpack_1.AngularCompilerPlugin(pluginOptions); -} -function getNonAotConfig(wco) { - const { tsConfigPath } = wco; - return { - module: { rules: [{ test: /\.tsx?$/, loader: webpack_1.NgToolsLoader }] }, - plugins: [_createAotPlugin(wco, { tsConfigPath, skipCodeGeneration: true })] - }; -} -exports.getNonAotConfig = getNonAotConfig; -function getAotConfig(wco, i18nExtract = false) { - const { tsConfigPath, buildOptions } = wco; - const loaders = [webpack_1.NgToolsLoader]; - if (buildOptions.buildOptimizer) { - loaders.unshift({ - loader: build_optimizer_1.buildOptimizerLoaderPath, - options: { sourceMap: buildOptions.sourceMap.scripts } - }); - } - const test = /(?:\.ngfactory\.js|\.ngstyle\.js|\.tsx?)$/; - const optimize = wco.buildOptions.optimization.scripts; - return { - module: { rules: [{ test, use: loaders }] }, - plugins: [ - _createAotPlugin(wco, { tsConfigPath, emitClassMetadata: !optimize, emitNgModuleScope: !optimize }, i18nExtract), - ], - }; -} -exports.getAotConfig = getAotConfig; -function getTypescriptWorkerPlugin(wco, workerTsConfigPath) { - const { buildOptions } = wco; - let pluginOptions = { - skipCodeGeneration: true, - tsConfigPath: workerTsConfigPath, - mainPath: undefined, - platform: webpack_1.PLATFORM.Browser, - sourceMap: buildOptions.sourceMap.scripts, - forkTypeChecker: buildOptions.forkTypeChecker, - contextElementDependencyConstructor: require('webpack/lib/dependencies/ContextElementDependency'), - logger: wco.logger, - // Run no transformers. - platformTransformers: [], - // Don't attempt lazy route discovery. - discoverLazyRoutes: false, - }; - pluginOptions = _pluginOptionsOverrides(buildOptions, pluginOptions); - return new webpack_1.AngularCompilerPlugin(pluginOptions); -} -exports.getTypescriptWorkerPlugin = getTypescriptWorkerPlugin; diff --git a/src/angular-cli-files/models/webpack-configs/utils.d.ts b/src/angular-cli-files/models/webpack-configs/utils.d.ts deleted file mode 100644 index 9713a793c..000000000 --- a/src/angular-cli-files/models/webpack-configs/utils.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { ExtraEntryPoint, ExtraEntryPointClass } from '../../../browser/schema'; -import { SourceMapDevToolPlugin } from 'webpack'; -import { ScriptTarget } from 'typescript'; -export interface HashFormat { - chunk: string; - extract: string; - file: string; - script: string; -} -export declare function getOutputHashFormat(option: string, length?: number): HashFormat; -declare type Omit = Pick>; -export declare type NormalizedEntryPoint = Required>; -export declare function normalizeExtraEntryPoints(extraEntryPoints: ExtraEntryPoint[], defaultBundleName: string): NormalizedEntryPoint[]; -export declare function getSourceMapDevTool(scriptsSourceMap: boolean | undefined, stylesSourceMap: boolean | undefined, hiddenSourceMap?: boolean, inlineSourceMap?: boolean): SourceMapDevToolPlugin; -/** - * Returns an ES version file suffix to differentiate between various builds. - */ -export declare function getEsVersionForFileName(scriptTargetOverride: ScriptTarget | undefined, esVersionInFileName?: boolean): string; -export declare function isPolyfillsEntry(name: string): boolean; -export {}; diff --git a/src/angular-cli-files/models/webpack-configs/utils.js b/src/angular-cli-files/models/webpack-configs/utils.js deleted file mode 100644 index 2b5fdffb7..000000000 --- a/src/angular-cli-files/models/webpack-configs/utils.js +++ /dev/null @@ -1,99 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. -Object.defineProperty(exports, "__esModule", { value: true }); -exports.isPolyfillsEntry = exports.getEsVersionForFileName = exports.getSourceMapDevTool = exports.normalizeExtraEntryPoints = exports.getOutputHashFormat = void 0; -const core_1 = require("@angular-devkit/core"); -const webpack_1 = require("webpack"); -const typescript_1 = require("typescript"); -function getOutputHashFormat(option, length = 20) { - const hashFormats = { - none: { chunk: '', extract: '', file: '', script: '' }, - media: { chunk: '', extract: '', file: `.[hash:${length}]`, script: '' }, - bundles: { - chunk: `.[chunkhash:${length}]`, - extract: `.[contenthash:${length}]`, - file: '', - script: `.[hash:${length}]`, - }, - all: { - chunk: `.[chunkhash:${length}]`, - extract: `.[contenthash:${length}]`, - file: `.[hash:${length}]`, - script: `.[hash:${length}]`, - }, - }; - return hashFormats[option] || hashFormats['none']; -} -exports.getOutputHashFormat = getOutputHashFormat; -function normalizeExtraEntryPoints(extraEntryPoints, defaultBundleName) { - return extraEntryPoints.map(entry => { - let normalizedEntry; - if (typeof entry === 'string') { - normalizedEntry = { input: entry, inject: true, bundleName: defaultBundleName }; - } - else { - const { lazy, inject = true, ...newEntry } = entry; - const injectNormalized = entry.lazy !== undefined ? !entry.lazy : inject; - let bundleName; - if (entry.bundleName) { - bundleName = entry.bundleName; - } - else if (!injectNormalized) { - // Lazy entry points use the file name as bundle name. - bundleName = core_1.basename(core_1.normalize(entry.input.replace(/\.(js|css|scss|sass|less|styl)$/i, ''))); - } - else { - bundleName = defaultBundleName; - } - normalizedEntry = { ...newEntry, inject: injectNormalized, bundleName }; - } - return normalizedEntry; - }); -} -exports.normalizeExtraEntryPoints = normalizeExtraEntryPoints; -function getSourceMapDevTool(scriptsSourceMap, stylesSourceMap, hiddenSourceMap = false, inlineSourceMap = false) { - const include = []; - if (scriptsSourceMap) { - include.push(/js$/); - } - if (stylesSourceMap) { - include.push(/css$/); - } - return new webpack_1.SourceMapDevToolPlugin({ - filename: inlineSourceMap ? undefined : '[file].map', - include, - // We want to set sourceRoot to `webpack:///` for non - // inline sourcemaps as otherwise paths to sourcemaps will be broken in browser - // `webpack:///` is needed for Visual Studio breakpoints to work properly as currently - // there is no way to set the 'webRoot' - sourceRoot: inlineSourceMap ? '' : 'webpack:///', - moduleFilenameTemplate: '[resource-path]', - append: hiddenSourceMap ? false : undefined, - }); -} -exports.getSourceMapDevTool = getSourceMapDevTool; -/** - * Returns an ES version file suffix to differentiate between various builds. - */ -function getEsVersionForFileName(scriptTargetOverride, esVersionInFileName = false) { - if (!esVersionInFileName || scriptTargetOverride === undefined) { - return ''; - } - if (scriptTargetOverride === typescript_1.ScriptTarget.ESNext) { - return '-esnext'; - } - return '-' + typescript_1.ScriptTarget[scriptTargetOverride].toLowerCase(); -} -exports.getEsVersionForFileName = getEsVersionForFileName; -function isPolyfillsEntry(name) { - return name === 'polyfills' || name === 'polyfills-es5'; -} -exports.isPolyfillsEntry = isPolyfillsEntry; diff --git a/src/angular-cli-files/models/webpack-configs/worker.d.ts b/src/angular-cli-files/models/webpack-configs/worker.d.ts deleted file mode 100644 index 3c76e1555..000000000 --- a/src/angular-cli-files/models/webpack-configs/worker.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Configuration } from 'webpack'; -import { WebpackConfigOptions } from '../build-options'; -export declare function getWorkerConfig(wco: WebpackConfigOptions): Configuration; diff --git a/src/angular-cli-files/models/webpack-configs/worker.js b/src/angular-cli-files/models/webpack-configs/worker.js deleted file mode 100644 index cb4f8d40c..000000000 --- a/src/angular-cli-files/models/webpack-configs/worker.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getWorkerConfig = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const path_1 = require("path"); -const typescript_1 = require("./typescript"); -const WorkerPlugin = require('worker-plugin'); -function getWorkerConfig(wco) { - const { buildOptions } = wco; - if (!buildOptions.webWorkerTsConfig) { - return {}; - } - if (typeof buildOptions.webWorkerTsConfig != 'string') { - throw new Error('The `webWorkerTsConfig` must be a string.'); - } - const workerTsConfigPath = path_1.resolve(wco.root, buildOptions.webWorkerTsConfig); - return { - plugins: [new WorkerPlugin({ - globalObject: false, - plugins: [typescript_1.getTypescriptWorkerPlugin(wco, workerTsConfigPath)], - })], - }; -} -exports.getWorkerConfig = getWorkerConfig; diff --git a/src/angular-cli-files/plugins/any-component-style-budget-checker.js b/src/angular-cli-files/plugins/any-component-style-budget-checker.js deleted file mode 100644 index 81a3ebdfb..000000000 --- a/src/angular-cli-files/plugins/any-component-style-budget-checker.js +++ /dev/null @@ -1,71 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.AnyComponentStyleBudgetChecker = void 0; -const path = require("path"); -const schema_1 = require("../../../src/browser/schema"); -const bundle_calculator_1 = require("../utilities/bundle-calculator"); -const PLUGIN_NAME = 'AnyComponentStyleBudgetChecker'; -/** - * Check budget sizes for component styles by emitting a warning or error if a - * budget is exceeded by a particular component's styles. - */ -class AnyComponentStyleBudgetChecker { - constructor(budgets) { - this.budgets = budgets.filter((budget) => budget.type === schema_1.Type.AnyComponentStyle); - } - apply(compiler) { - compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { - compilation.hooks.afterOptimizeChunkAssets.tap(PLUGIN_NAME, () => { - // In AOT compilations component styles get processed in child compilations. - // tslint:disable-next-line: no-any - const parentCompilation = compilation.compiler.parentCompilation; - if (!parentCompilation) { - return; - } - const cssExtensions = [ - '.css', - '.scss', - '.less', - '.styl', - '.sass', - ]; - const componentStyles = Object.keys(compilation.assets) - .filter((name) => cssExtensions.includes(path.extname(name))) - .map((name) => ({ - size: compilation.assets[name].size(), - label: name, - })); - const thresholds = flatMap(this.budgets, (budget) => bundle_calculator_1.calculateThresholds(budget)); - for (const { size, label } of componentStyles) { - for (const { severity, message } of bundle_calculator_1.checkThresholds(thresholds[Symbol.iterator](), size, label)) { - switch (severity) { - case bundle_calculator_1.ThresholdSeverity.Warning: - compilation.warnings.push(message); - break; - case bundle_calculator_1.ThresholdSeverity.Error: - compilation.errors.push(message); - break; - default: - assertNever(severity); - break; - } - } - } - }); - }); - } -} -exports.AnyComponentStyleBudgetChecker = AnyComponentStyleBudgetChecker; -function assertNever(input) { - throw new Error(`Unexpected call to assertNever() with input: ${JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`); -} -function flatMap(list, mapper) { - return [].concat(...list.map(mapper).map((iterator) => Array.from(iterator))); -} diff --git a/src/angular-cli-files/plugins/bundle-budget.d.ts b/src/angular-cli-files/plugins/bundle-budget.d.ts deleted file mode 100644 index 51f49f816..000000000 --- a/src/angular-cli-files/plugins/bundle-budget.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Compiler } from 'webpack'; -import { Budget } from '../../browser/schema'; -export interface BundleBudgetPluginOptions { - budgets: Budget[]; -} -export declare class BundleBudgetPlugin { - private options; - constructor(options: BundleBudgetPluginOptions); - apply(compiler: Compiler): void; - private runChecks; -} diff --git a/src/angular-cli-files/plugins/bundle-budget.js b/src/angular-cli-files/plugins/bundle-budget.js deleted file mode 100644 index c5bfe298a..000000000 --- a/src/angular-cli-files/plugins/bundle-budget.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BundleBudgetPlugin = void 0; -const bundle_calculator_1 = require("../utilities/bundle-calculator"); -class BundleBudgetPlugin { - constructor(options) { - this.options = options; - } - apply(compiler) { - const { budgets } = this.options; - if (!budgets || budgets.length === 0) { - return; - } - compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation) => { - this.runChecks(budgets, compilation); - }); - } - runChecks(budgets, compilation) { - // No process bundle results because this plugin is only used when differential - // builds are disabled. - const processResults = []; - const stats = compilation.getStats().toJson(); - for (const { severity, message } of bundle_calculator_1.checkBudgets(budgets, stats, processResults)) { - switch (severity) { - case bundle_calculator_1.ThresholdSeverity.Warning: - compilation.warnings.push(`budgets: ${message}`); - break; - case bundle_calculator_1.ThresholdSeverity.Error: - compilation.errors.push(`budgets: ${message}`); - break; - } - } - } -} -exports.BundleBudgetPlugin = BundleBudgetPlugin; diff --git a/src/angular-cli-files/plugins/common-js-usage-warn-plugin.js b/src/angular-cli-files/plugins/common-js-usage-warn-plugin.js deleted file mode 100644 index f75580a94..000000000 --- a/src/angular-cli-files/plugins/common-js-usage-warn-plugin.js +++ /dev/null @@ -1,78 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.CommonJsUsageWarnPlugin = void 0; -const path_1 = require("path"); -// Webpack doesn't export these so the deep imports can potentially break. -const CommonJsRequireDependency = require('webpack/lib/dependencies/CommonJsRequireDependency'); -const AMDDefineDependency = require('webpack/lib/dependencies/AMDDefineDependency'); -class CommonJsUsageWarnPlugin { - constructor(options = {}) { - this.options = options; - this.shownWarnings = new Set(); - // Allow the below depedency for HMR - // tslint:disable-next-line: max-line-length - // https://github.com/angular/angular-cli/blob/1e258317b1f6ec1e957ee3559cc3b28ba602f3ba/packages/angular_devkit/build_angular/src/dev-server/index.ts#L605-L638 - this.allowedDepedencies = [ - 'webpack/hot/dev-server', - ]; - if (this.options.allowedDepedencies) { - this.allowedDepedencies.push(...this.options.allowedDepedencies); - } - } - apply(compiler) { - compiler.hooks.compilation.tap('CommonJsUsageWarnPlugin', compilation => { - compilation.hooks.finishModules.tap('CommonJsUsageWarnPlugin', modules => { - var _a, _b, _c, _d; - for (const { dependencies, rawRequest, issuer } of modules) { - if (!rawRequest || - rawRequest.startsWith('.') || - path_1.isAbsolute(rawRequest)) { - // Skip if module is absolute or relative. - continue; - } - if ((_a = this.allowedDepedencies) === null || _a === void 0 ? void 0 : _a.includes(rawRequest)) { - // Skip as this module is allowed even if it's a CommonJS. - continue; - } - if (this.hasCommonJsDependencies(dependencies)) { - // Dependency is CommonsJS or AMD. - // Check if it's parent issuer is also a CommonJS dependency. - // In case it is skip as an warning will be show for the parent CommonJS dependency. - if (this.hasCommonJsDependencies((_c = (_b = issuer === null || issuer === void 0 ? void 0 : issuer.issuer) === null || _b === void 0 ? void 0 : _b.dependencies) !== null && _c !== void 0 ? _c : [])) { - continue; - } - // Find the main issuer (entry-point). - let mainIssuer = issuer; - while (mainIssuer === null || mainIssuer === void 0 ? void 0 : mainIssuer.issuer) { - mainIssuer = mainIssuer.issuer; - } - // Only show warnings for modules from main entrypoint. - // And if the issuer request is not from 'webpack-dev-server', as 'webpack-dev-server' - // will require CommonJS libraries for live reloading such as 'sockjs-node'. - if ((mainIssuer === null || mainIssuer === void 0 ? void 0 : mainIssuer.name) === 'main' && !((_d = issuer === null || issuer === void 0 ? void 0 : issuer.userRequest) === null || _d === void 0 ? void 0 : _d.includes('webpack-dev-server'))) { - const warning = `${issuer === null || issuer === void 0 ? void 0 : issuer.userRequest} depends on ${rawRequest}. CommonJS or AMD dependencies can cause optimization bailouts.\n` + - 'For more info see: https://web.dev/commonjs-larger-bundles\n' + - `To disable this warning add "${rawRequest}" to the "allowedCommonJsDependencies" option under "build" options in "angular.json".`; - // Avoid showing the same warning multiple times when in 'watch' mode. - if (!this.shownWarnings.has(warning)) { - compilation.warnings.push(warning); - this.shownWarnings.add(warning); - } - } - } - } - }); - }); - } - hasCommonJsDependencies(dependencies) { - return dependencies.some(d => d instanceof CommonJsRequireDependency || d instanceof AMDDefineDependency); - } -} -exports.CommonJsUsageWarnPlugin = CommonJsUsageWarnPlugin; diff --git a/src/angular-cli-files/plugins/dedupe-module-resolve-plugin.d.ts b/src/angular-cli-files/plugins/dedupe-module-resolve-plugin.d.ts deleted file mode 100644 index 998dba5c4..000000000 --- a/src/angular-cli-files/plugins/dedupe-module-resolve-plugin.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -interface NormalModuleFactoryRequest { - request: string; - context: { - issuer: string; - }; - relativePath: string; - path: string; - descriptionFileData: { - name: string; - version: string; - }; - descriptionFileRoot: string; - descriptionFilePath: string; -} -export interface DedupeModuleResolvePluginOptions { - verbose?: boolean; -} -/** - * DedupeModuleResolvePlugin is a webpack resolver plugin which dedupes modules with the same name and versions - * that are laid out in different parts of the node_modules tree. - * - * This is needed because Webpack relies on package managers to hoist modules and doesn't have any deduping logic. - */ -export declare class DedupeModuleResolvePlugin { - private options?; - modules: Map; - constructor(options?: DedupeModuleResolvePluginOptions | undefined); - apply(resolver: any): void; -} -export {}; diff --git a/src/angular-cli-files/plugins/dedupe-module-resolve-plugin.js b/src/angular-cli-files/plugins/dedupe-module-resolve-plugin.js deleted file mode 100644 index 47c0e5e43..000000000 --- a/src/angular-cli-files/plugins/dedupe-module-resolve-plugin.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DedupeModuleResolvePlugin = void 0; -/** - * DedupeModuleResolvePlugin is a webpack resolver plugin which dedupes modules with the same name and versions - * that are laid out in different parts of the node_modules tree. - * - * This is needed because Webpack relies on package managers to hoist modules and doesn't have any deduping logic. - */ -class DedupeModuleResolvePlugin { - constructor(options) { - this.options = options; - this.modules = new Map(); - } - // tslint:disable-next-line: no-any - apply(resolver) { - resolver - .getHook('before-described-relative') - .tapPromise('DedupeModuleResolvePlugin', async (request) => { - var _a; - if (request.relativePath !== '.') { - return; - } - const moduleId = request.descriptionFileData.name + '@' + request.descriptionFileData.version; - const prevResolvedModule = this.modules.get(moduleId); - if (!prevResolvedModule) { - // This is the first time we visit this module. - this.modules.set(moduleId, request); - return; - } - const { path, descriptionFilePath, descriptionFileRoot, } = prevResolvedModule; - if (request.path === path) { - // No deduping needed. - // Current path and previously resolved path are the same. - return; - } - if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.verbose) { - // tslint:disable-next-line: no-console - console.warn(`[DedupeModuleResolvePlugin]: ${request.path} -> ${path}`); - } - // Alter current request with previously resolved module. - request.path = path; - request.descriptionFileRoot = descriptionFileRoot; - request.descriptionFilePath = descriptionFilePath; - }); - } -} -exports.DedupeModuleResolvePlugin = DedupeModuleResolvePlugin; diff --git a/src/angular-cli-files/plugins/index-html-webpack-plugin.d.ts b/src/angular-cli-files/plugins/index-html-webpack-plugin.d.ts deleted file mode 100644 index 2288d9d2d..000000000 --- a/src/angular-cli-files/plugins/index-html-webpack-plugin.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Compiler } from 'webpack'; -import { CrossOriginValue } from '../utilities/index-file/augment-index-html'; -import { IndexHtmlTransform } from '../utilities/index-file/write-index-html'; -export interface IndexHtmlWebpackPluginOptions { - input: string; - output: string; - baseHref?: string; - entrypoints: string[]; - deployUrl?: string; - sri: boolean; - noModuleEntrypoints: string[]; - moduleEntrypoints: string[]; - postTransform?: IndexHtmlTransform; - crossOrigin?: CrossOriginValue; - lang?: string; -} -export declare class IndexHtmlWebpackPlugin { - private _options; - constructor(options?: Partial); - apply(compiler: Compiler): void; -} diff --git a/src/angular-cli-files/plugins/index-html-webpack-plugin.js b/src/angular-cli-files/plugins/index-html-webpack-plugin.js deleted file mode 100644 index c598c6b8a..000000000 --- a/src/angular-cli-files/plugins/index-html-webpack-plugin.js +++ /dev/null @@ -1,86 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.IndexHtmlWebpackPlugin = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const path = require("path"); -const webpack_sources_1 = require("webpack-sources"); -const augment_index_html_1 = require("../utilities/index-file/augment-index-html"); -const strip_bom_1 = require("../utilities/strip-bom"); -function readFile(filename, compilation) { - return new Promise((resolve, reject) => { - compilation.inputFileSystem.readFile(filename, (err, data) => { - if (err) { - reject(err); - return; - } - resolve(strip_bom_1.stripBom(data.toString())); - }); - }); -} -class IndexHtmlWebpackPlugin { - constructor(options) { - this._options = { - input: 'index.html', - output: 'index.html', - entrypoints: ['polyfills', 'main'], - noModuleEntrypoints: [], - moduleEntrypoints: [], - sri: false, - ...options, - }; - } - apply(compiler) { - compiler.hooks.emit.tapPromise('index-html-webpack-plugin', async (compilation) => { - // Get input html file - const inputContent = await readFile(this._options.input, compilation); - compilation.fileDependencies.add(this._options.input); - // Get all files for selected entrypoints - const files = []; - const noModuleFiles = []; - const moduleFiles = []; - for (const [entryName, entrypoint] of compilation.entrypoints) { - const entryFiles = ((entrypoint && entrypoint.getFiles()) || []).map((f) => ({ - name: entryName, - file: f, - extension: path.extname(f), - })); - if (this._options.noModuleEntrypoints.includes(entryName)) { - noModuleFiles.push(...entryFiles); - } - else if (this._options.moduleEntrypoints.includes(entryName)) { - moduleFiles.push(...entryFiles); - } - else { - files.push(...entryFiles); - } - } - const loadOutputFile = (name) => compilation.assets[name].source(); - let indexSource = await augment_index_html_1.augmentIndexHtml({ - input: this._options.input, - inputContent, - baseHref: this._options.baseHref, - deployUrl: this._options.deployUrl, - sri: this._options.sri, - crossOrigin: this._options.crossOrigin, - files, - noModuleFiles, - loadOutputFile, - moduleFiles, - entrypoints: this._options.entrypoints, - lang: this._options.lang, - }); - if (this._options.postTransform) { - indexSource = await this._options.postTransform(indexSource); - } - // Add to compilation assets - compilation.assets[this._options.output] = new webpack_sources_1.RawSource(indexSource); - }); - } -} -exports.IndexHtmlWebpackPlugin = IndexHtmlWebpackPlugin; diff --git a/src/angular-cli-files/plugins/karma-context.html b/src/angular-cli-files/plugins/karma-context.html deleted file mode 100644 index 1f91cd461..000000000 --- a/src/angular-cli-files/plugins/karma-context.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - %SCRIPTS% - - - - - - - - diff --git a/src/angular-cli-files/plugins/karma-debug.html b/src/angular-cli-files/plugins/karma-debug.html deleted file mode 100644 index 8c3fe3130..000000000 --- a/src/angular-cli-files/plugins/karma-debug.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - -%X_UA_COMPATIBLE% - Karma DEBUG RUNNER - - - - - - - - - - - - - - - %SCRIPTS% - - - - - - - - diff --git a/src/angular-cli-files/plugins/karma-webpack-failure-cb.d.ts b/src/angular-cli-files/plugins/karma-webpack-failure-cb.d.ts deleted file mode 100644 index df22e3d7d..000000000 --- a/src/angular-cli-files/plugins/karma-webpack-failure-cb.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -export declare class KarmaWebpackFailureCb { - private callback; - constructor(callback: (error: string | undefined, errors: string[]) => void); - apply(compiler: any): void; -} diff --git a/src/angular-cli-files/plugins/karma-webpack-failure-cb.js b/src/angular-cli-files/plugins/karma-webpack-failure-cb.js deleted file mode 100644 index 4396dbaeb..000000000 --- a/src/angular-cli-files/plugins/karma-webpack-failure-cb.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. -Object.defineProperty(exports, "__esModule", { value: true }); -exports.KarmaWebpackFailureCb = void 0; -// Force Webpack to throw compilation errors. Useful with karma-webpack when in single-run mode. -// Workaround for https://github.com/webpack-contrib/karma-webpack/issues/66 -class KarmaWebpackFailureCb { - constructor(callback) { - this.callback = callback; - } - apply(compiler) { - compiler.hooks.done.tap('KarmaWebpackFailureCb', (stats) => { - if (stats.compilation.errors.length > 0) { - this.callback(undefined, stats.compilation.errors.map((error) => error.message ? error.message : error.toString())); - } - }); - } -} -exports.KarmaWebpackFailureCb = KarmaWebpackFailureCb; diff --git a/src/angular-cli-files/plugins/karma.js b/src/angular-cli-files/plugins/karma.js deleted file mode 100644 index fa1025aeb..000000000 --- a/src/angular-cli-files/plugins/karma.js +++ /dev/null @@ -1,283 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. -Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const glob = require("glob"); -const webpack = require("webpack"); -const webpackDevMiddleware = require('webpack-dev-middleware'); -const karma_webpack_failure_cb_1 = require("./karma-webpack-failure-cb"); -const stats_1 = require("../utilities/stats"); -const stats_2 = require("../models/webpack-configs/stats"); -const node_1 = require("@angular-devkit/core/node"); -const index_1 = require("../../utils/index"); -/** - * Enumerate needed (but not require/imported) dependencies from this file - * to let the dependency validator know they are used. - * - * require('source-map-support') - * require('karma-source-map-support') - */ -let blocked = []; -let isBlocked = false; -let webpackMiddleware; -let successCb; -let failureCb; -// Add files to the Karma files array. -function addKarmaFiles(files, newFiles, prepend = false) { - const defaults = { - included: true, - served: true, - watched: true - }; - const processedFiles = newFiles - // Remove globs that do not match any files, otherwise Karma will show a warning for these. - .filter(file => glob.sync(file.pattern, { nodir: true }).length != 0) - // Fill in pattern properties with defaults. - .map(file => ({ ...defaults, ...file })); - // It's important to not replace the array, because - // karma already has a reference to the existing array. - if (prepend) { - files.unshift(...processedFiles); - } - else { - files.push(...processedFiles); - } -} -const init = (config, emitter, customFileHandlers) => { - if (!config.buildWebpack) { - throw new Error(`The '@angular-devkit/build-angular/plugins/karma' karma plugin is meant to` + - ` be used from within Angular CLI and will not work correctly outside of it.`); - } - const options = config.buildWebpack.options; - const logger = config.buildWebpack.logger || node_1.createConsoleLogger(); - successCb = config.buildWebpack.successCb; - failureCb = config.buildWebpack.failureCb; - // When using code-coverage, auto-add coverage-istanbul. - config.reporters = config.reporters || []; - if (options.codeCoverage && config.reporters.indexOf('coverage-istanbul') === -1) { - config.reporters.push('coverage-istanbul'); - } - // Add a reporter that fixes sourcemap urls. - if (index_1.normalizeSourceMaps(options.sourceMap).scripts) { - config.reporters.push('@angular-devkit/build-angular--sourcemap-reporter'); - // Code taken from https://github.com/tschaub/karma-source-map-support. - // We can't use it directly because we need to add it conditionally in this file, and karma - // frameworks cannot be added dynamically. - const smsPath = path.dirname(require.resolve('source-map-support')); - const ksmsPath = path.dirname(require.resolve('karma-source-map-support')); - addKarmaFiles(config.files, [ - { pattern: path.join(smsPath, 'browser-source-map-support.js'), watched: false }, - { pattern: path.join(ksmsPath, 'client.js'), watched: false } - ], true); - } - config.reporters.push('@angular-devkit/build-angular--event-reporter'); - // Add webpack config. - const webpackConfig = config.buildWebpack.webpackConfig; - const webpackMiddlewareConfig = { - // Hide webpack output because its noisy. - logLevel: 'error', - stats: false, - watchOptions: { poll: options.poll }, - publicPath: '/_karma_webpack_/', - }; - const compilationErrorCb = (error, errors) => { - // Notify potential listeners of the compile error - emitter.emit('compile_error', errors); - // Finish Karma run early in case of compilation error. - emitter.emit('run_complete', [], { exitCode: 1 }); - // Unblock any karma requests (potentially started using `karma run`) - unblock(); - }; - webpackConfig.plugins.push(new karma_webpack_failure_cb_1.KarmaWebpackFailureCb(compilationErrorCb)); - // Use existing config if any. - config.webpack = Object.assign(webpackConfig, config.webpack); - config.webpackMiddleware = Object.assign(webpackMiddlewareConfig, config.webpackMiddleware); - // Our custom context and debug files list the webpack bundles directly instead of using - // the karma files array. - config.customContextFile = `${__dirname}/karma-context.html`; - config.customDebugFile = `${__dirname}/karma-debug.html`; - // Add the request blocker and the webpack server fallback. - config.beforeMiddleware = config.beforeMiddleware || []; - config.beforeMiddleware.push('@angular-devkit/build-angular--blocker'); - config.middleware = config.middleware || []; - config.middleware.push('@angular-devkit/build-angular--fallback'); - // The webpack tier owns the watch behavior so we want to force it in the config. - webpackConfig.watch = !config.singleRun; - if (config.singleRun) { - // There's no option to turn off file watching in webpack-dev-server, but - // we can override the file watcher instead. - webpackConfig.plugins.unshift({ - apply: (compiler) => { - compiler.hooks.afterEnvironment.tap('karma', () => { - compiler.watchFileSystem = { watch: () => { } }; - }); - }, - }); - } - // Files need to be served from a custom path for Karma. - webpackConfig.output.path = '/_karma_webpack_/'; - webpackConfig.output.publicPath = '/_karma_webpack_/'; - let compiler; - try { - compiler = webpack(webpackConfig); - } - catch (e) { - logger.error(e.stack || e); - if (e.details) { - logger.error(e.details); - } - throw e; - } - function handler(callback) { - isBlocked = true; - if (typeof callback === 'function') { - callback(); - } - } - compiler.hooks.invalid.tap('karma', () => handler()); - compiler.hooks.watchRun.tapAsync('karma', (_, callback) => handler(callback)); - compiler.hooks.run.tapAsync('karma', (_, callback) => handler(callback)); - function unblock() { - isBlocked = false; - blocked.forEach((cb) => cb()); - blocked = []; - } - let lastCompilationHash; - const statsConfig = stats_2.getWebpackStatsConfig(); - compiler.hooks.done.tap('karma', (stats) => { - if (stats.compilation.errors.length > 0) { - const json = stats.toJson(config.stats); - // Print compilation errors. - logger.error(stats_1.statsErrorsToString(json, statsConfig)); - lastCompilationHash = undefined; - // Emit a failure build event if there are compilation errors. - failureCb && failureCb(); - } - else if (stats.hash != lastCompilationHash) { - // Refresh karma only when there are no webpack errors, and if the compilation changed. - lastCompilationHash = stats.hash; - emitter.refreshFiles(); - } - unblock(); - }); - webpackMiddleware = new webpackDevMiddleware(compiler, webpackMiddlewareConfig); - // Forward requests to webpack server. - customFileHandlers.push({ - urlRegex: /^\/_karma_webpack_\/.*/, - handler: function handler(req, res) { - webpackMiddleware(req, res, function () { - // Ensure script and style bundles are served. - // They are mentioned in the custom karma context page and we don't want them to 404. - const alwaysServe = [ - '/_karma_webpack_/runtime.js', - '/_karma_webpack_/polyfills.js', - '/_karma_webpack_/polyfills-es5.js', - '/_karma_webpack_/scripts.js', - '/_karma_webpack_/styles.js', - '/_karma_webpack_/vendor.js', - ]; - if (alwaysServe.indexOf(req.url) != -1) { - res.statusCode = 200; - res.end(); - } - else { - res.statusCode = 404; - res.end('Not found'); - } - }); - } - }); - emitter.on('exit', (done) => { - webpackMiddleware.close(); - done(); - }); -}; -init.$inject = ['config', 'emitter', 'customFileHandlers']; -// Block requests until the Webpack compilation is done. -function requestBlocker() { - return function (_request, _response, next) { - if (isBlocked) { - blocked.push(next); - } - else { - next(); - } - }; -} -// Copied from "karma-jasmine-diff-reporter" source code: -// In case, when multiple reporters are used in conjunction -// with initSourcemapReporter, they both will show repetitive log -// messages when displaying everything that supposed to write to terminal. -// So just suppress any logs from initSourcemapReporter by doing nothing on -// browser log, because it is an utility reporter, -// unless it's alone in the "reporters" option and base reporter is used. -function muteDuplicateReporterLogging(context, config) { - context.writeCommonMsg = function () { }; - const reporterName = '@angular/cli'; - const hasTrailingReporters = config.reporters.slice(-1).pop() !== reporterName; - if (hasTrailingReporters) { - context.writeCommonMsg = function () { }; - } -} -// Emits builder events. -const eventReporter = function (baseReporterDecorator, config) { - baseReporterDecorator(this); - muteDuplicateReporterLogging(this, config); - this.onRunComplete = function (_browsers, results) { - if (results.exitCode === 0) { - successCb && successCb(); - } - else { - failureCb && failureCb(); - } - }; - // avoid duplicate failure message - this.specFailure = () => { }; -}; -eventReporter.$inject = ['baseReporterDecorator', 'config']; -// Strip the server address and webpack scheme (webpack://) from error log. -const sourceMapReporter = function (baseReporterDecorator, config) { - baseReporterDecorator(this); - muteDuplicateReporterLogging(this, config); - const urlRegexp = /https:\/\/fanyv88.com:443\/http\/localhost:\d+\/_karma_webpack_\/webpack:\//gi; - this.onSpecComplete = function (_browser, result) { - if (!result.success && result.log.length > 0) { - result.log.forEach((log, idx) => { - result.log[idx] = log.replace(urlRegexp, ''); - }); - } - }; - // avoid duplicate complete message - this.onRunComplete = () => { }; - // avoid duplicate failure message - this.specFailure = () => { }; -}; -sourceMapReporter.$inject = ['baseReporterDecorator', 'config']; -// When a request is not found in the karma server, try looking for it from the webpack server root. -function fallbackMiddleware() { - return function (req, res, next) { - if (webpackMiddleware) { - const webpackUrl = '/_karma_webpack_' + req.url; - const webpackReq = { ...req, url: webpackUrl }; - webpackMiddleware(webpackReq, res, next); - } - else { - next(); - } - }; -} -module.exports = { - 'framework:@angular-devkit/build-angular': ['factory', init], - 'reporter:@angular-devkit/build-angular--sourcemap-reporter': ['type', sourceMapReporter], - 'reporter:@angular-devkit/build-angular--event-reporter': ['type', eventReporter], - 'middleware:@angular-devkit/build-angular--blocker': ['factory', requestBlocker], - 'middleware:@angular-devkit/build-angular--fallback': ['factory', fallbackMiddleware] -}; diff --git a/src/angular-cli-files/plugins/named-chunks-plugin.js b/src/angular-cli-files/plugins/named-chunks-plugin.js deleted file mode 100644 index 5d7e038c6..000000000 --- a/src/angular-cli-files/plugins/named-chunks-plugin.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.NamedLazyChunksPlugin = void 0; -// Webpack doesn't export these so the deep imports can potentially break. -// There doesn't seem to exist any ergonomic way to alter chunk names for non-context lazy chunks -// (https://github.com/webpack/webpack/issues/9075) so this is the best alternative for now. -const ImportDependency = require('webpack/lib/dependencies/ImportDependency'); -const ImportDependenciesBlock = require('webpack/lib/dependencies/ImportDependenciesBlock'); -const Template = require('webpack/lib/Template'); -class NamedLazyChunksPlugin { - constructor() { } - apply(compiler) { - compiler.hooks.compilation.tap('named-lazy-chunks-plugin', compilation => { - // The dependencyReference hook isn't in the webpack typings so we have to type it as any. - // tslint:disable-next-line: no-any - compilation.hooks.dependencyReference.tap('named-lazy-chunks-plugin', - // tslint:disable-next-line: no-any - (_, dependency) => { - if ( - // Check this dependency is from an `import()` statement. - dependency instanceof ImportDependency - && dependency.block instanceof ImportDependenciesBlock - // Don't rename chunks that already have a name. - && dependency.block.chunkName === null) { - // Convert the request to a valid chunk name using the same logic used - // in webpack/lib/ContextModule.js - dependency.block.chunkName = Template.toPath(dependency.request); - } - }); - }); - } -} -exports.NamedLazyChunksPlugin = NamedLazyChunksPlugin; diff --git a/src/angular-cli-files/plugins/optimize-css-webpack-plugin.d.ts b/src/angular-cli-files/plugins/optimize-css-webpack-plugin.d.ts deleted file mode 100644 index 3f8a4be48..000000000 --- a/src/angular-cli-files/plugins/optimize-css-webpack-plugin.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Compiler } from 'webpack'; -export interface OptimizeCssWebpackPluginOptions { - sourceMap: boolean; - test: (file: string) => boolean; -} -export declare class OptimizeCssWebpackPlugin { - private readonly _options; - constructor(options: Partial); - apply(compiler: Compiler): void; -} diff --git a/src/angular-cli-files/plugins/optimize-css-webpack-plugin.js b/src/angular-cli-files/plugins/optimize-css-webpack-plugin.js deleted file mode 100644 index c2cd1c287..000000000 --- a/src/angular-cli-files/plugins/optimize-css-webpack-plugin.js +++ /dev/null @@ -1,91 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.OptimizeCssWebpackPlugin = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const cssNano = require("cssnano"); -const webpack_sources_1 = require("webpack-sources"); -function hook(compiler, action) { - compiler.hooks.compilation.tap('optimize-css-webpack-plugin', (compilation) => { - compilation.hooks.optimizeChunkAssets.tapPromise('optimize-css-webpack-plugin', chunks => action(compilation, chunks)); - }); -} -class OptimizeCssWebpackPlugin { - constructor(options) { - this._options = { - sourceMap: false, - test: file => file.endsWith('.css'), - ...options, - }; - } - apply(compiler) { - hook(compiler, (compilation, chunks) => { - const files = [...compilation.additionalChunkAssets]; - chunks.forEach(chunk => { - if (chunk.files && chunk.files.length > 0) { - files.push(...chunk.files); - } - }); - const actions = files - .filter(file => this._options.test(file)) - .map(async (file) => { - const asset = compilation.assets[file]; - if (!asset) { - return; - } - let content; - // tslint:disable-next-line: no-any - let map; - if (this._options.sourceMap && asset.sourceAndMap) { - const sourceAndMap = asset.sourceAndMap(); - content = sourceAndMap.source; - map = sourceAndMap.map; - } - else { - content = asset.source(); - } - if (content.length === 0) { - return; - } - const cssNanoOptions = { - preset: ['default', { - // Disable SVG optimization, as this can cause optimizations which are not compatible in all browsers. - svgo: false, - }], - }; - const postCssOptions = { - from: file, - map: map && { annotation: false, prev: map }, - }; - const output = await new Promise((resolve, reject) => { - // the last parameter is not in the typings - // tslint:disable-next-line: no-any - cssNano.process(content, postCssOptions, cssNanoOptions) - .then(resolve) - .catch(reject); - }); - const warnings = output.warnings(); - if (warnings.length) { - compilation.warnings.push(...warnings.map(({ text }) => text)); - } - let newSource; - if (output.map) { - newSource = new webpack_sources_1.SourceMapSource(output.css, file, - // tslint:disable-next-line: no-any - output.map.toString(), content, map); - } - else { - newSource = new webpack_sources_1.RawSource(output.css); - } - compilation.assets[file] = newSource; - }); - return Promise.all(actions); - }); - } -} -exports.OptimizeCssWebpackPlugin = OptimizeCssWebpackPlugin; diff --git a/src/angular-cli-files/plugins/postcss-cli-resources.d.ts b/src/angular-cli-files/plugins/postcss-cli-resources.d.ts deleted file mode 100644 index 910d36de0..000000000 --- a/src/angular-cli-files/plugins/postcss-cli-resources.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as postcss from 'postcss'; -import * as webpack from 'webpack'; -export interface PostcssCliResourcesOptions { - baseHref?: string; - deployUrl?: string; - resourcesOutputPath?: string; - rebaseRootRelative?: boolean; - filename: string; - loader: webpack.loader.LoaderContext; - emitFile: boolean; -} -declare const _default: postcss.Plugin; -export default _default; diff --git a/src/angular-cli-files/plugins/remove-hash-plugin.js b/src/angular-cli-files/plugins/remove-hash-plugin.js deleted file mode 100644 index d3d93b273..000000000 --- a/src/angular-cli-files/plugins/remove-hash-plugin.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.RemoveHashPlugin = void 0; -class RemoveHashPlugin { - constructor(options) { - this.options = options; - } - apply(compiler) { - compiler.hooks.compilation.tap('remove-hash-plugin', compilation => { - const mainTemplate = compilation.mainTemplate; - mainTemplate.hooks.assetPath.tap('remove-hash-plugin', (path, data) => { - const chunkName = data.chunk && data.chunk.name; - const { chunkNames, hashFormat } = this.options; - if (chunkName && chunkNames.includes(chunkName)) { - // Replace hash formats with empty strings. - return path - .replace(hashFormat.chunk, '') - .replace(hashFormat.extract, ''); - } - return path; - }); - }); - } -} -exports.RemoveHashPlugin = RemoveHashPlugin; diff --git a/src/angular-cli-files/plugins/scripts-webpack-plugin.d.ts b/src/angular-cli-files/plugins/scripts-webpack-plugin.d.ts deleted file mode 100644 index 516b43665..000000000 --- a/src/angular-cli-files/plugins/scripts-webpack-plugin.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Compiler } from 'webpack'; -export interface ScriptsWebpackPluginOptions { - name: string; - sourceMap: boolean; - scripts: string[]; - filename: string; - basePath: string; -} -export declare class ScriptsWebpackPlugin { - private options; - private _lastBuildTime?; - private _cachedOutput?; - constructor(options?: Partial); - shouldSkip(compilation: any, scripts: string[]): boolean; - private _insertOutput; - apply(compiler: Compiler): void; -} diff --git a/src/angular-cli-files/plugins/scripts-webpack-plugin.js b/src/angular-cli-files/plugins/scripts-webpack-plugin.js deleted file mode 100644 index e1330f5d7..000000000 --- a/src/angular-cli-files/plugins/scripts-webpack-plugin.js +++ /dev/null @@ -1,111 +0,0 @@ -"use strict"; -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ScriptsWebpackPlugin = void 0; -const webpack_sources_1 = require("webpack-sources"); -const loader_utils_1 = require("loader-utils"); -const path = require("path"); -const Chunk = require('webpack/lib/Chunk'); -const EntryPoint = require('webpack/lib/Entrypoint'); -function addDependencies(compilation, scripts) { - for (const script of scripts) { - compilation.fileDependencies.add(script); - } -} -function hook(compiler, action) { - compiler.hooks.thisCompilation.tap('scripts-webpack-plugin', (compilation) => { - compilation.hooks.additionalAssets.tapAsync('scripts-webpack-plugin', (callback) => action(compilation, callback)); - }); -} -class ScriptsWebpackPlugin { - constructor(options = {}) { - this.options = options; - } - shouldSkip(compilation, scripts) { - if (this._lastBuildTime == undefined) { - this._lastBuildTime = Date.now(); - return false; - } - for (let i = 0; i < scripts.length; i++) { - const scriptTime = compilation.fileTimestamps.get(scripts[i]); - if (!scriptTime || scriptTime > this._lastBuildTime) { - this._lastBuildTime = Date.now(); - return false; - } - } - return true; - } - _insertOutput(compilation, { filename, source }, cached = false) { - const chunk = new Chunk(this.options.name); - chunk.rendered = !cached; - chunk.id = this.options.name; - chunk.ids = [chunk.id]; - chunk.files.push(filename); - const entrypoint = new EntryPoint(this.options.name); - entrypoint.pushChunk(chunk); - chunk.addGroup(entrypoint); - compilation.entrypoints.set(this.options.name, entrypoint); - compilation.chunks.push(chunk); - compilation.assets[filename] = source; - } - apply(compiler) { - if (!this.options.scripts || this.options.scripts.length === 0) { - return; - } - const scripts = this.options.scripts - .filter(script => !!script) - .map(script => path.resolve(this.options.basePath || '', script)); - hook(compiler, (compilation, callback) => { - if (this.shouldSkip(compilation, scripts)) { - if (this._cachedOutput) { - this._insertOutput(compilation, this._cachedOutput, true); - } - addDependencies(compilation, scripts); - callback(); - return; - } - const sourceGetters = scripts.map(fullPath => { - return new Promise((resolve, reject) => { - compilation.inputFileSystem.readFile(fullPath, (err, data) => { - if (err) { - reject(err); - return; - } - const content = data.toString(); - let source; - if (this.options.sourceMap) { - // TODO: Look for source map file (for '.min' scripts, etc.) - let adjustedPath = fullPath; - if (this.options.basePath) { - adjustedPath = path.relative(this.options.basePath, fullPath); - } - source = new webpack_sources_1.OriginalSource(content, adjustedPath); - } - else { - source = new webpack_sources_1.RawSource(content); - } - resolve(source); - }); - }); - }); - Promise.all(sourceGetters) - .then(sources => { - const concatSource = new webpack_sources_1.ConcatSource(); - sources.forEach(source => { - concatSource.add(source); - concatSource.add('\n;'); - }); - const combinedSource = new webpack_sources_1.CachedSource(concatSource); - const filename = loader_utils_1.interpolateName({ resourcePath: 'scripts.js' }, this.options.filename, { content: combinedSource.source() }); - const output = { filename, source: combinedSource }; - this._insertOutput(compilation, output); - this._cachedOutput = output; - addDependencies(compilation, scripts); - callback(); - }) - .catch((err) => callback(err)); - }); - } -} -exports.ScriptsWebpackPlugin = ScriptsWebpackPlugin; diff --git a/src/angular-cli-files/plugins/single-test-transform.d.ts b/src/angular-cli-files/plugins/single-test-transform.d.ts deleted file mode 100644 index 567490405..000000000 --- a/src/angular-cli-files/plugins/single-test-transform.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { logging } from '@angular-devkit/core'; -import { loader } from 'webpack'; -export interface SingleTestTransformLoaderOptions { - files?: string[]; - logger?: logging.Logger; -} -export declare const SingleTestTransformLoader: string; -/** - * This loader transforms the default test file to only run tests - * for some specs instead of all specs. - * It works by replacing the known content of the auto-generated test file: - * const context = require.context('./', true, /\.spec\.ts$/); - * context.keys().map(context); - * with: - * const context = { keys: () => ({ map: (_a) => { } }) }; - * context.keys().map(context); - * So that it does nothing. - * Then it adds import statements for each file in the files options - * array to import them directly, and thus run the tests there. - */ -export default function loader(this: loader.LoaderContext, source: string): string; diff --git a/src/angular-cli-files/plugins/single-test-transform.js b/src/angular-cli-files/plugins/single-test-transform.js deleted file mode 100644 index dd58b97bf..000000000 --- a/src/angular-cli-files/plugins/single-test-transform.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SingleTestTransformLoader = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const core_1 = require("@angular-devkit/core"); -const loader_utils_1 = require("loader-utils"); -const path_1 = require("path"); -exports.SingleTestTransformLoader = require.resolve(path_1.join(__dirname, 'single-test-transform')); -/** - * This loader transforms the default test file to only run tests - * for some specs instead of all specs. - * It works by replacing the known content of the auto-generated test file: - * const context = require.context('./', true, /\.spec\.ts$/); - * context.keys().map(context); - * with: - * const context = { keys: () => ({ map: (_a) => { } }) }; - * context.keys().map(context); - * So that it does nothing. - * Then it adds import statements for each file in the files options - * array to import them directly, and thus run the tests there. - */ -function loader(source) { - const { files = [], logger = console } = loader_utils_1.getOptions(this); - // signal the user that expected content is not present. - if (!source.includes('require.context(')) { - logger.error(core_1.tags.stripIndent `The 'include' option requires that the 'main' file for tests includes the below line: - const context = require.context('./', true, /\.spec\.ts$/); - Arguments passed to require.context are not strict and can be changed.`); - return source; - } - const targettedImports = files - .map(path => `require('./${path.replace('.' + path_1.extname(path), '')}');`) - .join('\n'); - const mockedRequireContext = 'Object.assign(() => { }, { keys: () => [], resolve: () => undefined });\n'; - source = source.replace(/require\.context\(.*/, mockedRequireContext + targettedImports); - return source; -} -exports.default = loader; diff --git a/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.d.ts b/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.d.ts deleted file mode 100644 index fdfed214a..000000000 --- a/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -export declare class SuppressExtractedTextChunksWebpackPlugin { - constructor(); - apply(compiler: any): void; -} diff --git a/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.js b/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.js deleted file mode 100644 index 77ad55896..000000000 --- a/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.js +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SuppressExtractedTextChunksWebpackPlugin = void 0; -// Remove .js files from entry points consisting entirely of .css|scss|sass|less|styl. -// To be used together with ExtractTextPlugin. -class SuppressExtractedTextChunksWebpackPlugin { - constructor() { } - apply(compiler) { - compiler.hooks.compilation.tap('SuppressExtractedTextChunks', (compilation) => { - // find which chunks have css only entry points - const cssOnlyChunks = []; - const entryPoints = compilation.options.entry; - // determine which entry points are composed entirely of css files - for (let entryPoint of Object.keys(entryPoints)) { - let entryFiles = entryPoints[entryPoint]; - // when type of entryFiles is not array, make it as an array - entryFiles = entryFiles instanceof Array ? entryFiles : [entryFiles]; - if (entryFiles.every((el) => el.match(/\.(css|scss|sass|less|styl)$/) !== null)) { - cssOnlyChunks.push(entryPoint); - } - } - // Remove the js file for supressed chunks - compilation.hooks.afterSeal.tap('SuppressExtractedTextChunks', () => { - compilation.chunks - .filter((chunk) => cssOnlyChunks.indexOf(chunk.name) !== -1) - .forEach((chunk) => { - let newFiles = []; - chunk.files.forEach((file) => { - if (file.match(/\.js(\.map)?$/)) { - // remove js files - delete compilation.assets[file]; - } - else { - newFiles.push(file); - } - }); - chunk.files = newFiles; - }); - }); - // Remove scripts tags with a css file as source, because HtmlWebpackPlugin will use - // a css file as a script for chunks without js files. - // TODO: Enable this once HtmlWebpackPlugin supports Webpack 4 - // compilation.plugin('html-webpack-plugin-alter-asset-tags', - // (htmlPluginData: any, callback: any) => { - // const filterFn = (tag: any) => - // !(tag.tagName === 'script' && tag.attributes.src.match(/\.css$/)); - // htmlPluginData.head = htmlPluginData.head.filter(filterFn); - // htmlPluginData.body = htmlPluginData.body.filter(filterFn); - // callback(null, htmlPluginData); - // }); - }); - } -} -exports.SuppressExtractedTextChunksWebpackPlugin = SuppressExtractedTextChunksWebpackPlugin; diff --git a/src/angular-cli-files/plugins/webpack-rollup-loader.d.ts b/src/angular-cli-files/plugins/webpack-rollup-loader.d.ts deleted file mode 100644 index 790966983..000000000 --- a/src/angular-cli-files/plugins/webpack-rollup-loader.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { RawSourceMap } from 'source-map'; -import webpack = require('webpack'); -export default function webpackRollupLoader(this: webpack.loader.LoaderContext, source: string, sourceMap: RawSourceMap): void; diff --git a/src/angular-cli-files/plugins/webpack-rollup-loader.js b/src/angular-cli-files/plugins/webpack-rollup-loader.js deleted file mode 100644 index be85062d4..000000000 --- a/src/angular-cli-files/plugins/webpack-rollup-loader.js +++ /dev/null @@ -1,113 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const path_1 = require("path"); -const rollup_1 = require("rollup"); -function splitRequest(request) { - const inx = request.lastIndexOf('!'); - if (inx === -1) { - return { - loaders: '', - resource: request, - }; - } - else { - return { - loaders: request.slice(0, inx + 1), - resource: request.slice(inx + 1), - }; - } -} -// Load resolve paths using Webpack. -function webpackResolutionPlugin(loaderContext, entryId, entryIdCodeAndMap) { - return { - name: 'webpack-resolution-plugin', - resolveId: (id, importerId) => { - if (id === entryId) { - return entryId; - } - else { - return new Promise((resolve, reject) => { - // split apart resource paths because Webpack's this.resolve() can't handle `loader!` - // prefixes - const parts = splitRequest(id); - const importerParts = splitRequest(importerId); - // resolve the full path of the imported file with Webpack's module loader - // this will figure out node_modules imports, Webpack aliases, etc. - loaderContext.resolve(path_1.dirname(importerParts.resource), parts.resource, (err, fullPath) => err ? reject(err) : resolve(parts.loaders + fullPath)); - }); - } - }, - load: (id) => { - if (id === entryId) { - return entryIdCodeAndMap; - } - return new Promise((resolve, reject) => { - // load the module with Webpack - // this will apply all relevant loaders, etc. - loaderContext.loadModule(id, (err, source, map) => err ? reject(err) : resolve({ code: source, map: map })); - }); - }, - }; -} -function webpackRollupLoader(source, sourceMap) { - // Note: this loader isn't cacheable because it will add the lazy chunks to the - // virtual file system on completion. - const callback = this.async(); - if (!callback) { - throw new Error('Async loader support is required.'); - } - const options = this.query || {}; - const entryId = this.resourcePath; - const sourcemap = this.sourceMap; - // Get the VirtualFileSystemDecorator that AngularCompilerPlugin added so we can write to it. - // Since we use webpackRollupLoader as a post loader, this should be there. - // TODO: we should be able to do this in a more elegant way by again decorating webpacks - // input file system inside a custom WebpackRollupPlugin, modelled after AngularCompilerPlugin. - const vfs = this._compiler.inputFileSystem; - const virtualWrite = (path, data) => vfs.getWebpackCompilerHost().writeFile(path, data, false); - // Bundle with Rollup - const rollupOptions = { - ...options, - input: entryId, - plugins: [ - ...(options.plugins || []), - webpackResolutionPlugin(this, entryId, { code: source, map: sourceMap }), - ], - }; - rollup_1.rollup(rollupOptions) - .then(build => build.generate({ format: 'es', sourcemap })) - .then((result) => { - const [mainChunk, ...otherChunksOrAssets] = result.output; - // Write other chunks and assets to the virtual file system so that webpack can load them. - const resultDir = path_1.dirname(entryId); - otherChunksOrAssets.forEach(chunkOrAsset => { - const { fileName, type } = chunkOrAsset; - if (type == 'chunk') { - const { code, map } = chunkOrAsset; - virtualWrite(path_1.join(resultDir, fileName), code); - if (map) { - // Also write the map if there's one. - // Probably need scriptsSourceMap set on CLI to load it. - virtualWrite(path_1.join(resultDir, `${fileName}.map`), map.toString()); - } - } - else if (type == 'asset') { - const { source } = chunkOrAsset; - // Source might be a Buffer. Just assuming it's a string for now. - virtualWrite(path_1.join(resultDir, fileName), source); - } - }); - // Always return the main chunk from webpackRollupLoader. - // Cast to any here is needed because of a typings incompatibility between source-map versions. - // tslint:disable-next-line:no-any - callback(null, mainChunk.code, mainChunk.map); - }, (err) => callback(err)); -} -exports.default = webpackRollupLoader; diff --git a/src/angular-cli-files/plugins/webpack.d.ts b/src/angular-cli-files/plugins/webpack.d.ts deleted file mode 100644 index 8db4395a6..000000000 --- a/src/angular-cli-files/plugins/webpack.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -export { AnyComponentStyleBudgetChecker } from './any-component-style-budget-checker'; -export { OptimizeCssWebpackPlugin, OptimizeCssWebpackPluginOptions } from './optimize-css-webpack-plugin'; -export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget'; -export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin'; -export { SuppressExtractedTextChunksWebpackPlugin } from './suppress-entry-chunks-webpack-plugin'; -export { RemoveHashPlugin, RemoveHashPluginOptions } from './remove-hash-plugin'; -export { NamedLazyChunksPlugin } from './named-chunks-plugin'; -export { DedupeModuleResolvePlugin } from './dedupe-module-resolve-plugin'; -export { CommonJsUsageWarnPlugin } from './common-js-usage-warn-plugin'; -export { default as PostcssCliResources, PostcssCliResourcesOptions, } from './postcss-cli-resources'; -export declare const WebpackRollupLoader: string; diff --git a/src/angular-cli-files/utilities/bundle-calculator.d.ts b/src/angular-cli-files/utilities/bundle-calculator.d.ts deleted file mode 100644 index 1f842b134..000000000 --- a/src/angular-cli-files/utilities/bundle-calculator.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as webpack from 'webpack'; -import { ProcessBundleResult } from '../../../src/utils/process-bundle'; -import { Budget } from '../../browser/schema'; -interface Threshold { - limit: number; - type: ThresholdType; - severity: ThresholdSeverity; -} -declare enum ThresholdType { - Max = "maximum", - Min = "minimum" -} -export declare enum ThresholdSeverity { - Warning = "warning", - Error = "error" -} -export declare function calculateThresholds(budget: Budget): IterableIterator; -export declare function checkBudgets(budgets: Budget[], webpackStats: webpack.Stats.ToJsonOutput, processResults: ProcessBundleResult[]): IterableIterator<{ - severity: ThresholdSeverity; - message: string; -}>; -export declare function checkThresholds(thresholds: IterableIterator, size: number, label?: string): IterableIterator<{ - severity: ThresholdSeverity; - message: string; -}>; -export {}; diff --git a/src/angular-cli-files/utilities/bundle-calculator.js b/src/angular-cli-files/utilities/bundle-calculator.js deleted file mode 100644 index a27b05fe1..000000000 --- a/src/angular-cli-files/utilities/bundle-calculator.js +++ /dev/null @@ -1,350 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.checkThresholds = exports.checkBudgets = exports.calculateThresholds = exports.ThresholdSeverity = void 0; -const schema_1 = require("../../browser/schema"); -const stats_1 = require("../utilities/stats"); -var ThresholdType; -(function (ThresholdType) { - ThresholdType["Max"] = "maximum"; - ThresholdType["Min"] = "minimum"; -})(ThresholdType || (ThresholdType = {})); -var ThresholdSeverity; -(function (ThresholdSeverity) { - ThresholdSeverity["Warning"] = "warning"; - ThresholdSeverity["Error"] = "error"; -})(ThresholdSeverity = exports.ThresholdSeverity || (exports.ThresholdSeverity = {})); -var DifferentialBuildType; -(function (DifferentialBuildType) { - // FIXME: this should match the actual file suffix and not hardcoded. - DifferentialBuildType["ORIGINAL"] = "es2015"; - DifferentialBuildType["DOWNLEVEL"] = "es5"; -})(DifferentialBuildType || (DifferentialBuildType = {})); -function* calculateThresholds(budget) { - if (budget.maximumWarning) { - yield { - limit: calculateBytes(budget.maximumWarning, budget.baseline, 1), - type: ThresholdType.Max, - severity: ThresholdSeverity.Warning, - }; - } - if (budget.maximumError) { - yield { - limit: calculateBytes(budget.maximumError, budget.baseline, 1), - type: ThresholdType.Max, - severity: ThresholdSeverity.Error, - }; - } - if (budget.minimumWarning) { - yield { - limit: calculateBytes(budget.minimumWarning, budget.baseline, -1), - type: ThresholdType.Min, - severity: ThresholdSeverity.Warning, - }; - } - if (budget.minimumError) { - yield { - limit: calculateBytes(budget.minimumError, budget.baseline, -1), - type: ThresholdType.Min, - severity: ThresholdSeverity.Error, - }; - } - if (budget.warning) { - yield { - limit: calculateBytes(budget.warning, budget.baseline, -1), - type: ThresholdType.Min, - severity: ThresholdSeverity.Warning, - }; - yield { - limit: calculateBytes(budget.warning, budget.baseline, 1), - type: ThresholdType.Max, - severity: ThresholdSeverity.Warning, - }; - } - if (budget.error) { - yield { - limit: calculateBytes(budget.error, budget.baseline, -1), - type: ThresholdType.Min, - severity: ThresholdSeverity.Error, - }; - yield { - limit: calculateBytes(budget.error, budget.baseline, 1), - type: ThresholdType.Max, - severity: ThresholdSeverity.Error, - }; - } -} -exports.calculateThresholds = calculateThresholds; -/** - * Calculates the sizes for bundles in the budget type provided. - */ -function calculateSizes(budget, stats, processResults) { - if (budget.type === schema_1.Type.AnyComponentStyle) { - // Component style size information is not available post-build, this must - // be checked mid-build via the `AnyComponentStyleBudgetChecker` plugin. - throw new Error('Can not calculate size of AnyComponentStyle. Use `AnyComponentStyleBudgetChecker` instead.'); - } - const calculatorMap = { - all: AllCalculator, - allScript: AllScriptCalculator, - any: AnyCalculator, - anyScript: AnyScriptCalculator, - bundle: BundleCalculator, - initial: InitialCalculator, - }; - const ctor = calculatorMap[budget.type]; - const { chunks, assets } = stats; - if (!chunks) { - throw new Error('Webpack stats output did not include chunk information.'); - } - if (!assets) { - throw new Error('Webpack stats output did not include asset information.'); - } - const calculator = new ctor(budget, chunks, assets, processResults); - return calculator.calculate(); -} -class Calculator { - constructor(budget, chunks, assets, processResults) { - this.budget = budget; - this.chunks = chunks; - this.assets = assets; - this.processResults = processResults; - } - /** Calculates the size of the given chunk for the provided build type. */ - calculateChunkSize(chunk, buildType) { - // Look for a process result containing different builds for this chunk. - const processResult = this.processResults - .find((processResult) => processResult.name === chunk.id.toString()); - if (processResult) { - // Found a differential build, use the correct size information. - const processResultFile = getDifferentialBuildResult(processResult, buildType); - return processResultFile && processResultFile.size || 0; - } - else { - // No differential builds, get the chunk size by summing its assets. - return chunk.files - .filter(file => !file.endsWith('.map')) - .map(file => { - const asset = this.assets.find((asset) => asset.name === file); - if (!asset) { - throw new Error(`Could not find asset for file: ${file}`); - } - return asset.size; - }) - .reduce((l, r) => l + r, 0); - } - } -} -/** - * A named bundle. - */ -class BundleCalculator extends Calculator { - calculate() { - const budgetName = this.budget.name; - if (!budgetName) { - return []; - } - // The chunk may or may not have differential builds. Compute the size for - // each then check afterwards if they are all the same. - const buildSizes = Object.values(DifferentialBuildType).map((buildType) => { - const size = this.chunks - .filter(chunk => chunk.names.indexOf(budgetName) !== -1) - .map(chunk => this.calculateChunkSize(chunk, buildType)) - .reduce((l, r) => l + r, 0); - return { size, label: `${this.budget.name}-${buildType}` }; - }); - // If this bundle was not actually generated by a differential build, then - // merge the results into a single value. - if (allEquivalent(buildSizes.map((buildSize) => buildSize.size))) { - return mergeDifferentialBuildSizes(buildSizes, budgetName); - } - else { - return buildSizes; - } - } -} -/** - * The sum of all initial chunks (marked as initial). - */ -class InitialCalculator extends Calculator { - calculate() { - const buildSizes = Object.values(DifferentialBuildType).map((buildType) => { - return { - label: `initial-${buildType}`, - size: this.chunks - .filter(chunk => chunk.initial) - .map(chunk => this.calculateChunkSize(chunk, buildType)) - .reduce((l, r) => l + r, 0), - }; - }); - // If this bundle was not actually generated by a differential build, then - // merge the results into a single value. - if (allEquivalent(buildSizes.map((buildSize) => buildSize.size))) { - return mergeDifferentialBuildSizes(buildSizes, 'initial'); - } - else { - return buildSizes; - } - } -} -/** - * The sum of all the scripts portions. - */ -class AllScriptCalculator extends Calculator { - calculate() { - const size = this.assets - .filter(asset => asset.name.endsWith('.js')) - .map(asset => asset.size) - .reduce((total, size) => total + size, 0); - return [{ size, label: 'total scripts' }]; - } -} -/** - * All scripts and assets added together. - */ -class AllCalculator extends Calculator { - calculate() { - const size = this.assets - .filter(asset => !asset.name.endsWith('.map')) - .map(asset => asset.size) - .reduce((total, size) => total + size, 0); - return [{ size, label: 'total' }]; - } -} -/** - * Any script, individually. - */ -class AnyScriptCalculator extends Calculator { - calculate() { - return this.assets - .filter(asset => asset.name.endsWith('.js')) - .map(asset => ({ - size: asset.size, - label: asset.name, - })); - } -} -/** - * Any script or asset (images, css, etc). - */ -class AnyCalculator extends Calculator { - calculate() { - return this.assets - .filter(asset => !asset.name.endsWith('.map')) - .map(asset => ({ - size: asset.size, - label: asset.name, - })); - } -} -/** - * Calculate the bytes given a string value. - */ -function calculateBytes(input, baseline, factor = 1) { - const matches = input.match(/^\s*(\d+(?:\.\d+)?)\s*(%|(?:[mM]|[kK]|[gG])?[bB])?\s*$/); - if (!matches) { - return NaN; - } - const baselineBytes = baseline && calculateBytes(baseline) || 0; - let value = Number(matches[1]); - switch (matches[2] && matches[2].toLowerCase()) { - case '%': - value = baselineBytes * value / 100; - break; - case 'kb': - value *= 1024; - break; - case 'mb': - value *= 1024 * 1024; - break; - case 'gb': - value *= 1024 * 1024 * 1024; - break; - } - if (baselineBytes === 0) { - return value; - } - return baselineBytes + value * factor; -} -function* checkBudgets(budgets, webpackStats, processResults) { - // Ignore AnyComponentStyle budgets as these are handled in `AnyComponentStyleBudgetChecker`. - const computableBudgets = budgets.filter((budget) => budget.type !== schema_1.Type.AnyComponentStyle); - for (const budget of computableBudgets) { - const sizes = calculateSizes(budget, webpackStats, processResults); - for (const { size, label } of sizes) { - yield* checkThresholds(calculateThresholds(budget), size, label); - } - } -} -exports.checkBudgets = checkBudgets; -function* checkThresholds(thresholds, size, label) { - for (const threshold of thresholds) { - switch (threshold.type) { - case ThresholdType.Max: { - if (size <= threshold.limit) { - continue; - } - const sizeDifference = stats_1.formatSize(size - threshold.limit); - yield { - severity: threshold.severity, - message: `Exceeded maximum budget for ${label}. Budget ${stats_1.formatSize(threshold.limit)} was not met by ${sizeDifference} with a total of ${stats_1.formatSize(size)}.`, - }; - break; - } - case ThresholdType.Min: { - if (size >= threshold.limit) { - continue; - } - const sizeDifference = stats_1.formatSize(threshold.limit - size); - yield { - severity: threshold.severity, - message: `Failed to meet minimum budget for ${label}. Budget ${stats_1.formatSize(threshold.limit)} was not met by ${sizeDifference} with a total of ${stats_1.formatSize(size)}.`, - }; - break; - } - default: { - assertNever(threshold.type); - break; - } - } - } -} -exports.checkThresholds = checkThresholds; -/** Returns the {@link ProcessBundleFile} for the given {@link DifferentialBuildType}. */ -function getDifferentialBuildResult(processResult, buildType) { - switch (buildType) { - case DifferentialBuildType.ORIGINAL: return processResult.original || null; - case DifferentialBuildType.DOWNLEVEL: return processResult.downlevel || null; - } -} -/** - * Merges the given differential builds into a single, non-differential value. - * - * Preconditions: All the sizes should be equivalent, or else they represent - * differential builds. - */ -function mergeDifferentialBuildSizes(buildSizes, margeLabel) { - if (buildSizes.length === 0) { - return []; - } - // Only one size. - return [{ - label: margeLabel, - size: buildSizes[0].size, - }]; -} -/** Returns whether or not all items in the list are equivalent to each other. */ -function allEquivalent(items) { - if (items.length === 0) { - return true; - } - const first = items[0]; - for (const item of items.slice(1)) { - if (item !== first) { - return false; - } - } - return true; -} -function assertNever(input) { - throw new Error(`Unexpected call to assertNever() with input: ${JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`); -} diff --git a/src/angular-cli-files/utilities/check-port.d.ts b/src/angular-cli-files/utilities/check-port.d.ts deleted file mode 100644 index 97cc1e4df..000000000 --- a/src/angular-cli-files/utilities/check-port.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare function checkPort(port: number, host: string, basePort?: number): Promise; diff --git a/src/angular-cli-files/utilities/check-port.js b/src/angular-cli-files/utilities/check-port.js deleted file mode 100644 index ce2f85497..000000000 --- a/src/angular-cli-files/utilities/check-port.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.checkPort = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const net = require("net"); -function checkPort(port, host, basePort = 49152) { - return new Promise((resolve, reject) => { - function _getPort(portNumber) { - if (portNumber > 65535) { - reject(new Error(`There is no port available.`)); - } - const server = net.createServer(); - server.once('error', (err) => { - if (err.code !== 'EADDRINUSE') { - reject(err); - } - else if (port === 0) { - _getPort(portNumber + 1); - } - else { - // If the port isn't available and we weren't looking for any port, throw error. - reject(new Error(`Port ${port} is already in use. Use '--port' to specify a different port.`)); - } - }) - .once('listening', () => { - server.close(); - resolve(portNumber); - }) - .listen(portNumber, host); - } - _getPort(port || basePort); - }); -} -exports.checkPort = checkPort; diff --git a/src/angular-cli-files/utilities/find-tests.d.ts b/src/angular-cli-files/utilities/find-tests.d.ts deleted file mode 100644 index 7bef8c204..000000000 --- a/src/angular-cli-files/utilities/find-tests.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare function findTests(patterns: string[], cwd: string, workspaceRoot: string): string[]; diff --git a/src/angular-cli-files/utilities/find-tests.js b/src/angular-cli-files/utilities/find-tests.js deleted file mode 100644 index c659fe485..000000000 --- a/src/angular-cli-files/utilities/find-tests.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.findTests = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const fs_1 = require("fs"); -const glob = require("glob"); -const path_1 = require("path"); -const is_directory_1 = require("./is-directory"); -// go through all patterns and find unique list of files -function findTests(patterns, cwd, workspaceRoot) { - return patterns.reduce((files, pattern) => { - const relativePathToMain = cwd.replace(workspaceRoot, '').substr(1); // remove leading slash - const tests = findMatchingTests(pattern, cwd, relativePathToMain); - tests.forEach(file => { - if (!files.includes(file)) { - files.push(file); - } - }); - return files; - }, []); -} -exports.findTests = findTests; -function findMatchingTests(pattern, cwd, relativePathToMain) { - // normalize pattern, glob lib only accepts forward slashes - pattern = pattern.replace(/\\/g, '/'); - relativePathToMain = relativePathToMain.replace(/\\/g, '/'); - // remove relativePathToMain to support relative paths from root - // such paths are easy to get when running scripts via IDEs - if (pattern.startsWith(relativePathToMain + '/')) { - pattern = pattern.substr(relativePathToMain.length + 1); // +1 to include slash - } - // special logic when pattern does not look like a glob - if (!glob.hasMagic(pattern)) { - if (is_directory_1.isDirectory(path_1.join(cwd, pattern))) { - pattern = `${pattern}/**/*.spec.@(ts|tsx)`; - } - else { - // see if matching spec file exists - const extension = path_1.extname(pattern); - const matchingSpec = `${path_1.basename(pattern, extension)}.spec${extension}`; - if (fs_1.existsSync(path_1.join(cwd, path_1.dirname(pattern), matchingSpec))) { - pattern = path_1.join(path_1.dirname(pattern), matchingSpec).replace(/\\/g, '/'); - } - } - } - const files = glob.sync(pattern, { - cwd, - }); - return files; -} diff --git a/src/angular-cli-files/utilities/find-up.d.ts b/src/angular-cli-files/utilities/find-up.d.ts deleted file mode 100644 index 8de29db9c..000000000 --- a/src/angular-cli-files/utilities/find-up.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export declare function findUp(names: string | string[], from: string, stopOnNodeModules?: boolean): string | null; -export declare function findAllNodeModules(from: string, root?: string): string[]; diff --git a/src/angular-cli-files/utilities/find-up.js b/src/angular-cli-files/utilities/find-up.js deleted file mode 100644 index 95dfe5e0b..000000000 --- a/src/angular-cli-files/utilities/find-up.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.findAllNodeModules = exports.findUp = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const fs_1 = require("fs"); -const path = require("path"); -const is_directory_1 = require("./is-directory"); -function findUp(names, from, stopOnNodeModules = false) { - if (!Array.isArray(names)) { - names = [names]; - } - const root = path.parse(from).root; - let currentDir = from; - while (currentDir && currentDir !== root) { - for (const name of names) { - const p = path.join(currentDir, name); - if (fs_1.existsSync(p)) { - return p; - } - } - if (stopOnNodeModules) { - const nodeModuleP = path.join(currentDir, 'node_modules'); - if (fs_1.existsSync(nodeModuleP)) { - return null; - } - } - currentDir = path.dirname(currentDir); - } - return null; -} -exports.findUp = findUp; -function findAllNodeModules(from, root) { - const nodeModules = []; - let current = from; - while (current && current !== root) { - const potential = path.join(current, 'node_modules'); - if (fs_1.existsSync(potential) && is_directory_1.isDirectory(potential)) { - nodeModules.push(potential); - } - const next = path.dirname(current); - if (next === current) { - break; - } - current = next; - } - return nodeModules; -} -exports.findAllNodeModules = findAllNodeModules; diff --git a/src/angular-cli-files/utilities/index-file/augment-index-html.d.ts b/src/angular-cli-files/utilities/index-file/augment-index-html.d.ts deleted file mode 100644 index a1eeb0ec4..000000000 --- a/src/angular-cli-files/utilities/index-file/augment-index-html.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -export declare type LoadOutputFileFunctionType = (file: string) => Promise; -export declare type CrossOriginValue = 'none' | 'anonymous' | 'use-credentials'; -export interface AugmentIndexHtmlOptions { - input: string; - inputContent: string; - baseHref?: string; - deployUrl?: string; - sri: boolean; - /** crossorigin attribute setting of elements that provide CORS support */ - crossOrigin?: CrossOriginValue; - files: FileInfo[]; - /** Files that should be added using 'nomodule'. */ - noModuleFiles?: FileInfo[]; - /** Files that should be added using 'module'. */ - moduleFiles?: FileInfo[]; - loadOutputFile: LoadOutputFileFunctionType; - /** Used to sort the inseration of files in the HTML file */ - entrypoints: string[]; - /** Used to set the document default locale */ - lang?: string; -} -export interface FileInfo { - file: string; - name: string; - extension: string; -} -export declare function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise; diff --git a/src/angular-cli-files/utilities/index-file/augment-index-html.js b/src/angular-cli-files/utilities/index-file/augment-index-html.js deleted file mode 100644 index c9385c99f..000000000 --- a/src/angular-cli-files/utilities/index-file/augment-index-html.js +++ /dev/null @@ -1,208 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.augmentIndexHtml = void 0; -const crypto_1 = require("crypto"); -const webpack_sources_1 = require("webpack-sources"); -const parse5 = require('parse5'); -/* - * Helper function used by the IndexHtmlWebpackPlugin. - * Can also be directly used by builder, e. g. in order to generate an index.html - * after processing several configurations in order to build different sets of - * bundles for differential serving. - */ -// tslint:disable-next-line: no-big-function -async function augmentIndexHtml(params) { - const { loadOutputFile, files, noModuleFiles = [], moduleFiles = [], entrypoints } = params; - let { crossOrigin = 'none' } = params; - if (params.sri && crossOrigin === 'none') { - crossOrigin = 'anonymous'; - } - const stylesheets = new Set(); - const scripts = new Set(); - // Sort files in the order we want to insert them by entrypoint and dedupes duplicates - const mergedFiles = [...moduleFiles, ...noModuleFiles, ...files]; - for (const entrypoint of entrypoints) { - for (const { extension, file, name } of mergedFiles) { - if (name !== entrypoint) { - continue; - } - switch (extension) { - case '.js': - scripts.add(file); - break; - case '.css': - stylesheets.add(file); - break; - } - } - } - // Find the head and body elements - const treeAdapter = parse5.treeAdapters.default; - const document = parse5.parse(params.inputContent, { treeAdapter, locationInfo: true }); - let headElement; - let bodyElement; - let htmlElement; - for (const docChild of document.childNodes) { - if (docChild.tagName === 'html') { - htmlElement = docChild; - for (const htmlChild of docChild.childNodes) { - if (htmlChild.tagName === 'head') { - headElement = htmlChild; - } - else if (htmlChild.tagName === 'body') { - bodyElement = htmlChild; - } - } - } - } - if (!headElement || !bodyElement) { - throw new Error('Missing head and/or body elements'); - } - // Determine script insertion point - let scriptInsertionPoint; - if (bodyElement.__location && bodyElement.__location.endTag) { - scriptInsertionPoint = bodyElement.__location.endTag.startOffset; - } - else { - // Less accurate fallback - // parse5 4.x does not provide locations if malformed html is present - scriptInsertionPoint = params.inputContent.indexOf(''); - } - let styleInsertionPoint; - if (headElement.__location && headElement.__location.endTag) { - styleInsertionPoint = headElement.__location.endTag.startOffset; - } - else { - // Less accurate fallback - // parse5 4.x does not provide locations if malformed html is present - styleInsertionPoint = params.inputContent.indexOf(''); - } - // Inject into the html - const indexSource = new webpack_sources_1.ReplaceSource(new webpack_sources_1.RawSource(params.inputContent), params.input); - let scriptElements = ''; - for (const script of scripts) { - const attrs = [ - { name: 'src', value: (params.deployUrl || '') + script }, - ]; - if (crossOrigin !== 'none') { - attrs.push({ name: 'crossorigin', value: crossOrigin }); - } - // We want to include nomodule or module when a file is not common amongs all - // such as runtime.js - const scriptPredictor = ({ file }) => file === script; - if (!files.some(scriptPredictor)) { - // in some cases for differential loading file with the same name is available in both - // nomodule and module such as scripts.js - // we shall not add these attributes if that's the case - const isNoModuleType = noModuleFiles.some(scriptPredictor); - const isModuleType = moduleFiles.some(scriptPredictor); - if (isNoModuleType && !isModuleType) { - attrs.push({ name: 'nomodule', value: null }, { name: 'defer', value: null }); - } - else if (isModuleType && !isNoModuleType) { - attrs.push({ name: 'type', value: 'module' }); - } - else { - attrs.push({ name: 'defer', value: null }); - } - } - else { - attrs.push({ name: 'defer', value: null }); - } - if (params.sri) { - const content = await loadOutputFile(script); - attrs.push(..._generateSriAttributes(content)); - } - const attributes = attrs - .map(attr => (attr.value === null ? attr.name : `${attr.name}="${attr.value}"`)) - .join(' '); - scriptElements += ``; - } - indexSource.insert(scriptInsertionPoint, scriptElements); - // Adjust base href if specified - if (typeof params.baseHref == 'string') { - let baseElement; - for (const headChild of headElement.childNodes) { - if (headChild.tagName === 'base') { - baseElement = headChild; - } - } - const baseFragment = treeAdapter.createDocumentFragment(); - if (!baseElement) { - baseElement = treeAdapter.createElement('base', undefined, [ - { name: 'href', value: params.baseHref }, - ]); - treeAdapter.appendChild(baseFragment, baseElement); - indexSource.insert(headElement.__location.startTag.endOffset, parse5.serialize(baseFragment, { treeAdapter })); - } - else { - let hrefAttribute; - for (const attribute of baseElement.attrs) { - if (attribute.name === 'href') { - hrefAttribute = attribute; - } - } - if (hrefAttribute) { - hrefAttribute.value = params.baseHref; - } - else { - baseElement.attrs.push({ name: 'href', value: params.baseHref }); - } - treeAdapter.appendChild(baseFragment, baseElement); - indexSource.replace(baseElement.__location.startOffset, baseElement.__location.endOffset, parse5.serialize(baseFragment, { treeAdapter })); - } - } - const styleElements = treeAdapter.createDocumentFragment(); - for (const stylesheet of stylesheets) { - const attrs = [ - { name: 'rel', value: 'stylesheet' }, - { name: 'href', value: (params.deployUrl || '') + stylesheet }, - ]; - if (crossOrigin !== 'none') { - attrs.push({ name: 'crossorigin', value: crossOrigin }); - } - if (params.sri) { - const content = await loadOutputFile(stylesheet); - attrs.push(..._generateSriAttributes(content)); - } - const element = treeAdapter.createElement('link', undefined, attrs); - treeAdapter.appendChild(styleElements, element); - } - indexSource.insert(styleInsertionPoint, parse5.serialize(styleElements, { treeAdapter })); - // Adjust document locale if specified - if (typeof params.lang == 'string') { - const htmlFragment = treeAdapter.createDocumentFragment(); - let langAttribute; - for (const attribute of htmlElement.attrs) { - if (attribute.name === 'lang') { - langAttribute = attribute; - } - } - if (langAttribute) { - langAttribute.value = params.lang; - } - else { - htmlElement.attrs.push({ name: 'lang', value: params.lang }); - } - // we want only openning tag - htmlElement.childNodes = []; - treeAdapter.appendChild(htmlFragment, htmlElement); - indexSource.replace(htmlElement.__location.startTag.startOffset, htmlElement.__location.startTag.endOffset - 1, parse5.serialize(htmlFragment, { treeAdapter }).replace('', '')); - } - return indexSource.source(); -} -exports.augmentIndexHtml = augmentIndexHtml; -function _generateSriAttributes(content) { - const algo = 'sha384'; - const hash = crypto_1.createHash(algo) - .update(content, 'utf8') - .digest('base64'); - return [{ name: 'integrity', value: `${algo}-${hash}` }]; -} diff --git a/src/angular-cli-files/utilities/index-file/write-index-html.d.ts b/src/angular-cli-files/utilities/index-file/write-index-html.d.ts deleted file mode 100644 index 6a0708686..000000000 --- a/src/angular-cli-files/utilities/index-file/write-index-html.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { EmittedFiles } from '@angular-devkit/build-webpack'; -import { Path, virtualFs } from '@angular-devkit/core'; -import { Observable } from 'rxjs'; -import { ExtraEntryPoint } from '../../../browser/schema'; -import { CrossOriginValue } from './augment-index-html'; -export interface WriteIndexHtmlOptions { - host: virtualFs.Host; - outputPath: Path; - indexPath: Path; - files?: EmittedFiles[]; - noModuleFiles?: EmittedFiles[]; - moduleFiles?: EmittedFiles[]; - baseHref?: string; - deployUrl?: string; - sri?: boolean; - scripts?: ExtraEntryPoint[]; - styles?: ExtraEntryPoint[]; - postTransform?: IndexHtmlTransform; - crossOrigin?: CrossOriginValue; - lang?: string; -} -export declare type IndexHtmlTransform = (content: string) => Promise; -export declare function writeIndexHtml({ host, outputPath, indexPath, files, noModuleFiles, moduleFiles, baseHref, deployUrl, sri, scripts, styles, postTransform, crossOrigin, lang, }: WriteIndexHtmlOptions): Observable; diff --git a/src/angular-cli-files/utilities/index-file/write-index-html.js b/src/angular-cli-files/utilities/index-file/write-index-html.js deleted file mode 100644 index 0191e6fd2..000000000 --- a/src/angular-cli-files/utilities/index-file/write-index-html.js +++ /dev/null @@ -1,50 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.writeIndexHtml = void 0; -const core_1 = require("@angular-devkit/core"); -const rxjs_1 = require("rxjs"); -const operators_1 = require("rxjs/operators"); -const package_chunk_sort_1 = require("../package-chunk-sort"); -const strip_bom_1 = require("../strip-bom"); -const augment_index_html_1 = require("./augment-index-html"); -function writeIndexHtml({ host, outputPath, indexPath, files = [], noModuleFiles = [], moduleFiles = [], baseHref, deployUrl, sri = false, scripts = [], styles = [], postTransform, crossOrigin, lang, }) { - return host.read(indexPath).pipe(operators_1.map(content => strip_bom_1.stripBom(core_1.virtualFs.fileBufferToString(content))), operators_1.switchMap(content => augment_index_html_1.augmentIndexHtml({ - input: core_1.getSystemPath(outputPath), - inputContent: content, - baseHref, - deployUrl, - crossOrigin, - sri, - entrypoints: package_chunk_sort_1.generateEntryPoints({ scripts, styles }), - files: filterAndMapBuildFiles(files, ['.js', '.css']), - noModuleFiles: filterAndMapBuildFiles(noModuleFiles, '.js'), - moduleFiles: filterAndMapBuildFiles(moduleFiles, '.js'), - loadOutputFile: async (filePath) => { - return host - .read(core_1.join(core_1.dirname(outputPath), filePath)) - .pipe(operators_1.map(data => core_1.virtualFs.fileBufferToString(data))) - .toPromise(); - }, - lang: lang, - })), operators_1.switchMap(content => (postTransform ? postTransform(content) : rxjs_1.of(content))), operators_1.map(content => core_1.virtualFs.stringToFileBuffer(content)), operators_1.switchMap(content => host.write(outputPath, content))); -} -exports.writeIndexHtml = writeIndexHtml; -function filterAndMapBuildFiles(files, extensionFilter) { - const filteredFiles = []; - const validExtensions = Array.isArray(extensionFilter) - ? extensionFilter - : [extensionFilter]; - for (const { file, name, extension, initial } of files) { - if (name && initial && validExtensions.includes(extension)) { - filteredFiles.push({ file, extension, name }); - } - } - return filteredFiles; -} diff --git a/src/angular-cli-files/utilities/is-directory.d.ts b/src/angular-cli-files/utilities/is-directory.d.ts deleted file mode 100644 index 21cd7e0f8..000000000 --- a/src/angular-cli-files/utilities/is-directory.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -export declare function isDirectory(path: string): boolean; diff --git a/src/angular-cli-files/utilities/is-directory.js b/src/angular-cli-files/utilities/is-directory.js deleted file mode 100644 index 70fb24ad5..000000000 --- a/src/angular-cli-files/utilities/is-directory.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. -Object.defineProperty(exports, "__esModule", { value: true }); -exports.isDirectory = void 0; -const fs = require("fs"); -function isDirectory(path) { - try { - return fs.statSync(path).isDirectory(); - } - catch (_) { - return false; - } -} -exports.isDirectory = isDirectory; diff --git a/src/angular-cli-files/utilities/package-chunk-sort.d.ts b/src/angular-cli-files/utilities/package-chunk-sort.d.ts deleted file mode 100644 index 0aeb0fc88..000000000 --- a/src/angular-cli-files/utilities/package-chunk-sort.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { ExtraEntryPoint } from '../../browser/schema'; -export declare function generateEntryPoints(appConfig: { - styles: ExtraEntryPoint[]; - scripts: ExtraEntryPoint[]; -}): string[]; diff --git a/src/angular-cli-files/utilities/package-chunk-sort.js b/src/angular-cli-files/utilities/package-chunk-sort.js deleted file mode 100644 index 7f3ab23e2..000000000 --- a/src/angular-cli-files/utilities/package-chunk-sort.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.generateEntryPoints = void 0; -const utils_1 = require("../models/webpack-configs/utils"); -function generateEntryPoints(appConfig) { - // Add all styles/scripts, except lazy-loaded ones. - const extraEntryPoints = (extraEntryPoints, defaultBundleName) => { - const entryPoints = utils_1.normalizeExtraEntryPoints(extraEntryPoints, defaultBundleName) - .filter(entry => entry.inject) - .map(entry => entry.bundleName); - // remove duplicates - return [...new Set(entryPoints)]; - }; - const entryPoints = [ - 'runtime', - 'polyfills-es5', - 'polyfills', - 'sw-register', - ...extraEntryPoints(appConfig.styles, 'styles'), - ...extraEntryPoints(appConfig.scripts, 'scripts'), - 'vendor', - 'main', - ]; - const duplicates = [ - ...new Set(entryPoints.filter(x => entryPoints.indexOf(x) !== entryPoints.lastIndexOf(x))), - ]; - if (duplicates.length > 0) { - throw new Error(`Multiple bundles have been named the same: '${duplicates.join(`', '`)}'.`); - } - return entryPoints; -} -exports.generateEntryPoints = generateEntryPoints; diff --git a/src/angular-cli-files/utilities/read-tsconfig.js b/src/angular-cli-files/utilities/read-tsconfig.js deleted file mode 100644 index 4327b10cc..000000000 --- a/src/angular-cli-files/utilities/read-tsconfig.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.readTsconfig = void 0; -const path = require("path"); -/** - * Reads and parses a given TsConfig file. - * - * @param tsconfigPath - An absolute or relative path from 'workspaceRoot' of the tsconfig file. - * @param workspaceRoot - workspaceRoot root location when provided - * it will resolve 'tsconfigPath' from this path. - */ -function readTsconfig(tsconfigPath, workspaceRoot) { - const tsConfigFullPath = workspaceRoot - ? path.resolve(workspaceRoot, tsconfigPath) - : tsconfigPath; - // We use 'ng' instead of 'ts' here because 'ts' is not aware of 'angularCompilerOptions' - // and will not merged them if they are at un upper level tsconfig file when using `extends`. - const ng = require('@angular/compiler-cli'); - const configResult = ng.readConfiguration(tsConfigFullPath); - if (configResult.errors && configResult.errors.length) { - throw new Error(ng.formatDiagnostics(configResult.errors)); - } - return configResult; -} -exports.readTsconfig = readTsconfig; diff --git a/src/angular-cli-files/utilities/service-worker/index.d.ts b/src/angular-cli-files/utilities/service-worker/index.d.ts deleted file mode 100644 index 822b5551d..000000000 --- a/src/angular-cli-files/utilities/service-worker/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Path, virtualFs } from '@angular-devkit/core'; -export declare function augmentAppWithServiceWorker(host: virtualFs.Host, projectRoot: Path, appRoot: Path, outputPath: Path, baseHref: string, ngswConfigPath?: string): Promise; diff --git a/src/angular-cli-files/utilities/service-worker/index.js b/src/angular-cli-files/utilities/service-worker/index.js deleted file mode 100644 index aebbe8a46..000000000 --- a/src/angular-cli-files/utilities/service-worker/index.js +++ /dev/null @@ -1,98 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.augmentAppWithServiceWorker = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const core_1 = require("@angular-devkit/core"); -const crypto = require("crypto"); -class CliFilesystem { - constructor(_host, base) { - this._host = _host; - this.base = base; - } - list(path) { - return this._recursiveList(this._resolve(path), []).catch(() => []); - } - async read(path) { - return core_1.virtualFs.fileBufferToString(await this._readIntoBuffer(path)); - } - async hash(path) { - const sha1 = crypto.createHash('sha1'); - sha1.update(Buffer.from(await this._readIntoBuffer(path))); - return sha1.digest('hex'); - } - write(path, content) { - return this._host.write(this._resolve(path), core_1.virtualFs.stringToFileBuffer(content)) - .toPromise(); - } - _readIntoBuffer(path) { - return this._host.read(this._resolve(path)).toPromise(); - } - _resolve(path) { - return core_1.join(core_1.normalize(this.base), core_1.normalize(path)); - } - async _recursiveList(path, items) { - const fragments = await this._host.list(path).toPromise(); - for (const fragment of fragments) { - const item = core_1.join(path, fragment); - if (await this._host.isDirectory(item).toPromise()) { - await this._recursiveList(item, items); - } - else { - items.push('/' + core_1.relative(core_1.normalize(this.base), item)); - } - } - return items; - } -} -async function augmentAppWithServiceWorker(host, projectRoot, appRoot, outputPath, baseHref, ngswConfigPath) { - const distPath = core_1.normalize(outputPath); - const systemProjectRoot = core_1.getSystemPath(projectRoot); - // Find the service worker package - const workerPath = core_1.normalize(require.resolve('@angular/service-worker/ngsw-worker.js', { paths: [systemProjectRoot] })); - const swConfigPath = require.resolve('@angular/service-worker/config', { paths: [systemProjectRoot] }); - // Determine the configuration file path - let configPath; - if (ngswConfigPath) { - configPath = core_1.normalize(ngswConfigPath); - } - else { - configPath = core_1.join(appRoot, 'ngsw-config.json'); - } - // Ensure the configuration file exists - const configExists = await host.exists(configPath).toPromise(); - if (!configExists) { - throw new Error(core_1.tags.oneLine ` - Error: Expected to find an ngsw-config.json configuration - file in the ${core_1.getSystemPath(appRoot)} folder. Either provide one or disable Service Worker - in your angular.json configuration file. - `); - } - // Read the configuration file - const config = JSON.parse(core_1.virtualFs.fileBufferToString(await host.read(configPath).toPromise())); - // Generate the manifest - const GeneratorConstructor = require(swConfigPath).Generator; - const generator = new GeneratorConstructor(new CliFilesystem(host, outputPath), baseHref); - const output = await generator.process(config); - // Write the manifest - const manifest = JSON.stringify(output, null, 2); - await host.write(core_1.join(distPath, 'ngsw.json'), core_1.virtualFs.stringToFileBuffer(manifest)).toPromise(); - // Write the worker code - // NOTE: This is inefficient (kernel -> userspace -> kernel). - // `fs.copyFile` would be a better option but breaks the host abstraction - const workerCode = await host.read(workerPath).toPromise(); - await host.write(core_1.join(distPath, 'ngsw-worker.js'), workerCode).toPromise(); - // If present, write the safety worker code - const safetyPath = core_1.join(core_1.dirname(workerPath), 'safety-worker.js'); - if (await host.exists(safetyPath).toPromise()) { - const safetyCode = await host.read(safetyPath).toPromise(); - await host.write(core_1.join(distPath, 'worker-basic.min.js'), safetyCode).toPromise(); - await host.write(core_1.join(distPath, 'safety-worker.js'), safetyCode).toPromise(); - } -} -exports.augmentAppWithServiceWorker = augmentAppWithServiceWorker; diff --git a/src/angular-cli-files/utilities/stats.d.ts b/src/angular-cli-files/utilities/stats.d.ts deleted file mode 100644 index 7eb698715..000000000 --- a/src/angular-cli-files/utilities/stats.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -export declare function formatSize(size: number): string; -export declare function generateBundleStats(info: { - id: string | number; - size?: number; - files: string[]; - names?: string[]; - entry: boolean; - initial: boolean; - rendered?: boolean; -}, colors: boolean): string; -export declare function generateBuildStats(hash: string, time: number, colors: boolean): string; -export declare function statsToString(json: any, statsConfig: any): string; -export declare function statsWarningsToString(json: any, statsConfig: any): string; -export declare function statsErrorsToString(json: any, statsConfig: any): string; diff --git a/src/angular-cli-files/utilities/stats.js b/src/angular-cli-files/utilities/stats.js deleted file mode 100644 index b7d95df67..000000000 --- a/src/angular-cli-files/utilities/stats.js +++ /dev/null @@ -1,93 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.statsErrorsToString = exports.statsWarningsToString = exports.statsToString = exports.generateBuildStats = exports.generateBundleStats = exports.formatSize = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. -const core_1 = require("@angular-devkit/core"); -const path = require("path"); -const { bold, green, red, reset, white, yellow } = core_1.terminal; -function formatSize(size) { - if (size <= 0) { - return '0 bytes'; - } - const abbreviations = ['bytes', 'kB', 'MB', 'GB']; - const index = Math.floor(Math.log(size) / Math.log(1024)); - return `${+(size / Math.pow(1024, index)).toPrecision(3)} ${abbreviations[index]}`; -} -exports.formatSize = formatSize; -function generateBundleStats(info, colors) { - const g = (x) => (colors ? bold(green(x)) : x); - const y = (x) => (colors ? bold(yellow(x)) : x); - const id = info.id ? y(info.id.toString()) : ''; - const size = typeof info.size === 'number' ? ` ${formatSize(info.size)}` : ''; - const files = info.files.map(f => path.basename(f)).join(', '); - const names = info.names ? ` (${info.names.join(', ')})` : ''; - const initial = y(info.entry ? '[entry]' : info.initial ? '[initial]' : ''); - const flags = ['rendered', 'recorded'] - .map(f => (f && info[f] ? g(` [${f}]`) : '')) - .join(''); - return `chunk {${id}} ${g(files)}${names}${size} ${initial}${flags}`; -} -exports.generateBundleStats = generateBundleStats; -function generateBuildStats(hash, time, colors) { - const w = (x) => colors ? bold(white(x)) : x; - return `Date: ${w(new Date().toISOString())} - Hash: ${w(hash)} - Time: ${w('' + time)}ms`; -} -exports.generateBuildStats = generateBuildStats; -function statsToString(json, statsConfig) { - const colors = statsConfig.colors; - const rs = (x) => colors ? reset(x) : x; - const w = (x) => colors ? bold(white(x)) : x; - const changedChunksStats = json.chunks - .filter((chunk) => chunk.rendered) - .map((chunk) => { - const assets = json.assets.filter((asset) => chunk.files.indexOf(asset.name) != -1); - const summedSize = assets.filter((asset) => !asset.name.endsWith(".map")).reduce((total, asset) => { return total + asset.size; }, 0); - return generateBundleStats({ ...chunk, size: summedSize }, colors); - }); - const unchangedChunkNumber = json.chunks.length - changedChunksStats.length; - if (unchangedChunkNumber > 0) { - return '\n' + rs(core_1.tags.stripIndents ` - Date: ${w(new Date().toISOString())} - Hash: ${w(json.hash)} - ${unchangedChunkNumber} unchanged chunks - ${changedChunksStats.join('\n')} - Time: ${w('' + json.time)}ms - `); - } - else { - return '\n' + rs(core_1.tags.stripIndents ` - ${changedChunksStats.join('\n')} - Date: ${w(new Date().toISOString())} - Hash: ${w(json.hash)} - Time: ${w('' + json.time)}ms - `); - } -} -exports.statsToString = statsToString; -// TODO(#16193): Don't emit this warning in the first place rather than just suppressing it. -const ERRONEOUS_WARNINGS = [ - /multiple assets emit different content.*3rdpartylicenses\.txt/i, -]; -function statsWarningsToString(json, statsConfig) { - const colors = statsConfig.colors; - const rs = (x) => colors ? reset(x) : x; - const y = (x) => colors ? bold(yellow(x)) : x; - return rs('\n' + json.warnings - .map((warning) => `${warning}`) - .filter((warning) => !ERRONEOUS_WARNINGS.some((erroneous) => erroneous.test(warning))) - .map((warning) => y(`WARNING in ${warning}`)) - .join('\n\n')); -} -exports.statsWarningsToString = statsWarningsToString; -function statsErrorsToString(json, statsConfig) { - const colors = statsConfig.colors; - const rs = (x) => colors ? reset(x) : x; - const r = (x) => colors ? bold(red(x)) : x; - return rs('\n' + json.errors.map((error) => r(`ERROR in ${error}`)).join('\n')); -} -exports.statsErrorsToString = statsErrorsToString; diff --git a/src/angular-cli-files/utilities/strip-bom.d.ts b/src/angular-cli-files/utilities/strip-bom.d.ts deleted file mode 100644 index 428d515fa..000000000 --- a/src/angular-cli-files/utilities/strip-bom.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -export declare function stripBom(data: string): string; diff --git a/src/angular-cli-files/utilities/strip-bom.js b/src/angular-cli-files/utilities/strip-bom.js deleted file mode 100644 index ff3952c52..000000000 --- a/src/angular-cli-files/utilities/strip-bom.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. -Object.defineProperty(exports, "__esModule", { value: true }); -exports.stripBom = void 0; -// Strip BOM from file data. -// https://stackoverflow.com/questions/24356713 -function stripBom(data) { - return data.replace(/^\uFEFF/, ''); -} -exports.stripBom = stripBom; diff --git a/src/app-shell/index.d.ts b/src/app-shell/index.d.ts deleted file mode 100644 index 1a55c6f52..000000000 --- a/src/app-shell/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { JsonObject } from '@angular-devkit/core'; -import { Schema as BuildWebpackAppShellSchema } from './schema'; -declare const _default: import("@angular-devkit/architect/src/internal").Builder; -export default _default; diff --git a/src/app-shell/index.js b/src/app-shell/index.js deleted file mode 100644 index 0a333da20..000000000 --- a/src/app-shell/index.js +++ /dev/null @@ -1,123 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const architect_1 = require("@angular-devkit/architect"); -const core_1 = require("@angular-devkit/core"); -const node_1 = require("@angular-devkit/core/node"); -const fs = require("fs"); -const path = require("path"); -const service_worker_1 = require("../angular-cli-files/utilities/service-worker"); -async function _renderUniversal(options, context, browserResult, serverResult) { - // Get browser target options. - const browserTarget = architect_1.targetFromTargetString(options.browserTarget); - const rawBrowserOptions = await context.getTargetOptions(browserTarget); - const browserBuilderName = await context.getBuilderNameForTarget(browserTarget); - const browserOptions = await context.validateOptions(rawBrowserOptions, browserBuilderName); - // Initialize zone.js - const root = context.workspaceRoot; - const zonePackage = require.resolve('zone.js', { paths: [root] }); - await Promise.resolve().then(() => require(zonePackage)); - const host = new node_1.NodeJsSyncHost(); - const projectName = context.target && context.target.project; - if (!projectName) { - throw new Error('The builder requires a target.'); - } - const projectMetadata = await context.getProjectMetadata(projectName); - const projectRoot = core_1.resolve(core_1.normalize(root), core_1.normalize(projectMetadata.root || '')); - for (const outputPath of browserResult.outputPaths) { - const localeDirectory = path.relative(browserResult.baseOutputPath, outputPath); - const browserIndexOutputPath = path.join(outputPath, 'index.html'); - const indexHtml = fs.readFileSync(browserIndexOutputPath, 'utf8'); - const serverBundlePath = await _getServerModuleBundlePath(options, context, serverResult, localeDirectory); - const { AppServerModule, AppServerModuleNgFactory, renderModule, renderModuleFactory, } = await Promise.resolve().then(() => require(serverBundlePath)); - let renderModuleFn; - let AppServerModuleDef; - if (renderModuleFactory && AppServerModuleNgFactory) { - renderModuleFn = renderModuleFactory; - AppServerModuleDef = AppServerModuleNgFactory; - } - else if (renderModule && AppServerModule) { - renderModuleFn = renderModule; - AppServerModuleDef = AppServerModule; - } - else { - throw new Error(`renderModule method and/or AppServerModule were not exported from: ${serverBundlePath}.`); - } - // Load platform server module renderer - const renderOpts = { - document: indexHtml, - url: options.route, - }; - const html = await renderModuleFn(AppServerModuleDef, renderOpts); - // Overwrite the client index file. - const outputIndexPath = options.outputIndexPath - ? path.join(root, options.outputIndexPath) - : browserIndexOutputPath; - fs.writeFileSync(outputIndexPath, html); - if (browserOptions.serviceWorker) { - await service_worker_1.augmentAppWithServiceWorker(host, core_1.normalize(root), projectRoot, core_1.normalize(outputPath), browserOptions.baseHref || '/', browserOptions.ngswConfigPath); - } - } - return browserResult; -} -async function _getServerModuleBundlePath(options, context, serverResult, browserLocaleDirectory) { - if (options.appModuleBundle) { - return path.join(context.workspaceRoot, options.appModuleBundle); - } - else { - const { baseOutputPath = '' } = serverResult; - const outputPath = path.join(baseOutputPath, browserLocaleDirectory); - if (!fs.existsSync(outputPath)) { - throw new Error(`Could not find server output directory: ${outputPath}.`); - } - const files = fs.readdirSync(outputPath, 'utf8'); - const re = /^main\.(?:[a-zA-Z0-9]{20}\.)?(?:bundle\.)?js$/; - const maybeMain = files.filter(x => re.test(x))[0]; - if (!maybeMain) { - throw new Error('Could not find the main bundle.'); - } - else { - return path.join(outputPath, maybeMain); - } - } -} -async function _appShellBuilder(options, context) { - const browserTarget = architect_1.targetFromTargetString(options.browserTarget); - const serverTarget = architect_1.targetFromTargetString(options.serverTarget); - // Never run the browser target in watch mode. - // If service worker is needed, it will be added in _renderUniversal(); - const browserTargetRun = await context.scheduleTarget(browserTarget, { - watch: false, - serviceWorker: false, - }); - const serverTargetRun = await context.scheduleTarget(serverTarget, { - watch: false, - }); - try { - const [browserResult, serverResult] = await Promise.all([ - browserTargetRun.result, - serverTargetRun.result, - ]); - if (browserResult.success === false || browserResult.baseOutputPath === undefined) { - return browserResult; - } - else if (serverResult.success === false) { - return serverResult; - } - return await _renderUniversal(options, context, browserResult, serverResult); - } - catch (err) { - return { success: false, error: err.message }; - } - finally { - // Just be good citizens and stop those jobs. - await Promise.all([browserTargetRun.stop(), serverTargetRun.stop()]); - } -} -exports.default = architect_1.createBuilder(_appShellBuilder); diff --git a/src/browser/index.d.ts b/src/browser/index.d.ts deleted file mode 100644 index 743a4b15d..000000000 --- a/src/browser/index.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -/// -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; -import { WebpackLoggingCallback } from '@angular-devkit/build-webpack'; -import { json, logging, virtualFs } from '@angular-devkit/core'; -import * as fs from 'fs'; -import { Observable } from 'rxjs'; -import * as webpack from 'webpack'; -import { IndexHtmlTransform } from '../angular-cli-files/utilities/index-file/write-index-html'; -import { ExecutionTransformer } from '../transforms'; -import { I18nOptions } from '../utils/i18n-options'; -import { Schema as BrowserBuilderSchema } from './schema'; -export declare type BrowserBuilderOutput = json.JsonObject & BuilderOutput & { - baseOutputPath: string; - outputPaths: string[]; - /** - * @deprecated in version 9. Use 'outputPaths' instead. - */ - outputPath: string; -}; -export declare function createBrowserLoggingCallback(verbose: boolean, logger: logging.LoggerApi): WebpackLoggingCallback; -interface ConfigFromContextReturn { - config: webpack.Configuration; - projectRoot: string; - projectSourceRoot?: string; -} -export declare function buildBrowserWebpackConfigFromContext(options: BrowserBuilderSchema, context: BuilderContext, host: virtualFs.Host, i18n: boolean): Promise; -export declare function buildBrowserWebpackConfigFromContext(options: BrowserBuilderSchema, context: BuilderContext, host?: virtualFs.Host): Promise; -export declare function buildWebpackBrowser(options: BrowserBuilderSchema, context: BuilderContext, transforms?: { - webpackConfiguration?: ExecutionTransformer; - logging?: WebpackLoggingCallback; - indexHtml?: IndexHtmlTransform; -}): Observable; -declare const _default: import("@angular-devkit/architect/src/internal").Builder; -export default _default; diff --git a/src/browser/index.js b/src/browser/index.js deleted file mode 100644 index b663e992b..000000000 --- a/src/browser/index.js +++ /dev/null @@ -1,546 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.buildWebpackBrowser = exports.buildBrowserWebpackConfigFromContext = exports.createBrowserLoggingCallback = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const architect_1 = require("@angular-devkit/architect"); -const build_webpack_1 = require("@angular-devkit/build-webpack"); -const core_1 = require("@angular-devkit/core"); -const node_1 = require("@angular-devkit/core/node"); -const fs = require("fs"); -const path = require("path"); -const rxjs_1 = require("rxjs"); -const operators_1 = require("rxjs/operators"); -const typescript_1 = require("typescript"); -const analytics_1 = require("../../plugins/webpack/analytics"); -const webpack_configs_1 = require("../angular-cli-files/models/webpack-configs"); -const async_chunks_1 = require("../angular-cli-files/utilities/async-chunks"); -const bundle_calculator_1 = require("../angular-cli-files/utilities/bundle-calculator"); -const write_index_html_1 = require("../angular-cli-files/utilities/index-file/write-index-html"); -const read_tsconfig_1 = require("../angular-cli-files/utilities/read-tsconfig"); -const service_worker_1 = require("../angular-cli-files/utilities/service-worker"); -const stats_1 = require("../angular-cli-files/utilities/stats"); -const utils_1 = require("../utils"); -const action_executor_1 = require("../utils/action-executor"); -const cache_path_1 = require("../utils/cache-path"); -const copy_assets_1 = require("../utils/copy-assets"); -const environment_options_1 = require("../utils/environment-options"); -const i18n_inlining_1 = require("../utils/i18n-inlining"); -const output_paths_1 = require("../utils/output-paths"); -const version_1 = require("../utils/version"); -const webpack_browser_config_1 = require("../utils/webpack-browser-config"); -const cacheDownlevelPath = environment_options_1.cachingDisabled ? undefined : cache_path_1.findCachePath('angular-build-dl'); -function createBrowserLoggingCallback(verbose, logger) { - return (stats, config) => { - // config.stats contains our own stats settings, added during buildWebpackConfig(). - const json = stats.toJson(config.stats); - if (verbose) { - logger.info(stats.toString(config.stats)); - } - else { - logger.info(stats_1.statsToString(json, config.stats)); - } - if (stats.hasWarnings()) { - logger.warn(stats_1.statsWarningsToString(json, config.stats)); - } - if (stats.hasErrors()) { - logger.error(stats_1.statsErrorsToString(json, config.stats)); - } - }; -} -exports.createBrowserLoggingCallback = createBrowserLoggingCallback; -async function buildBrowserWebpackConfigFromContext(options, context, host = new node_1.NodeJsSyncHost(), i18n = false) { - const webpackPartialGenerator = (wco) => [ - webpack_configs_1.getCommonConfig(wco), - webpack_configs_1.getBrowserConfig(wco), - webpack_configs_1.getStylesConfig(wco), - webpack_configs_1.getStatsConfig(wco), - getAnalyticsConfig(wco, context), - getCompilerConfig(wco), - wco.buildOptions.webWorkerTsConfig ? webpack_configs_1.getWorkerConfig(wco) : {}, - ]; - if (i18n) { - return webpack_browser_config_1.generateI18nBrowserWebpackConfigFromContext(options, context, webpackPartialGenerator, host); - } - return webpack_browser_config_1.generateBrowserWebpackConfigFromContext(options, context, webpackPartialGenerator, host); -} -exports.buildBrowserWebpackConfigFromContext = buildBrowserWebpackConfigFromContext; -function getAnalyticsConfig(wco, context) { - if (context.analytics) { - // If there's analytics, add our plugin. Otherwise no need to slow down the build. - let category = 'build'; - if (context.builder) { - // We already vetted that this is a "safe" package, otherwise the analytics would be noop. - category = - context.builder.builderName.split(':')[1] || context.builder.builderName || 'build'; - } - // The category is the builder name if it's an angular builder. - return { - plugins: [new analytics_1.NgBuildAnalyticsPlugin(wco.projectRoot, context.analytics, category, !!wco.tsConfig.options.enableIvy)], - }; - } - return {}; -} -function getCompilerConfig(wco) { - if (wco.buildOptions.main || wco.buildOptions.polyfills) { - return wco.buildOptions.aot ? webpack_configs_1.getAotConfig(wco) : webpack_configs_1.getNonAotConfig(wco); - } - return {}; -} -async function initialize(options, context, host, webpackConfigurationTransform) { - const originalOutputPath = options.outputPath; - const { config, projectRoot, projectSourceRoot, i18n, } = await buildBrowserWebpackConfigFromContext(options, context, host, true); - let transformedConfig; - if (webpackConfigurationTransform) { - transformedConfig = await webpackConfigurationTransform(config); - } - if (options.deleteOutputPath) { - utils_1.deleteOutputDir(context.workspaceRoot, originalOutputPath); - } - return { config: transformedConfig || config, projectRoot, projectSourceRoot, i18n }; -} -// tslint:disable-next-line: no-big-function -function buildWebpackBrowser(options, context, transforms = {}) { - const host = new node_1.NodeJsSyncHost(); - const root = core_1.normalize(context.workspaceRoot); - const baseOutputPath = path.resolve(context.workspaceRoot, options.outputPath); - let outputPaths; - // Check Angular version. - version_1.assertCompatibleAngularVersion(context.workspaceRoot, context.logger); - return rxjs_1.from(initialize(options, context, host, transforms.webpackConfiguration)).pipe( - // tslint:disable-next-line: no-big-function - operators_1.switchMap(({ config, projectRoot, projectSourceRoot, i18n }) => { - const tsConfig = read_tsconfig_1.readTsconfig(options.tsConfig, context.workspaceRoot); - const target = tsConfig.options.target || typescript_1.ScriptTarget.ES5; - const buildBrowserFeatures = new utils_1.BuildBrowserFeatures(projectRoot, target); - const isDifferentialLoadingNeeded = buildBrowserFeatures.isDifferentialLoadingNeeded(); - if (target > typescript_1.ScriptTarget.ES2015 && isDifferentialLoadingNeeded) { - context.logger.warn(core_1.tags.stripIndent ` - WARNING: Using differential loading with targets ES5 and ES2016 or higher may - cause problems. Browsers with support for ES2015 will load the ES2016+ scripts - referenced with script[type="module"] but they may not support ES2016+ syntax. - `); - } - const useBundleDownleveling = isDifferentialLoadingNeeded && !options.watch; - const startTime = Date.now(); - return build_webpack_1.runWebpack(config, context, { - webpackFactory: require('webpack'), - logging: transforms.logging || - (useBundleDownleveling - ? () => { } - : createBrowserLoggingCallback(!!options.verbose, context.logger)), - }).pipe( - // tslint:disable-next-line: no-big-function - operators_1.concatMap(async (buildEvent) => { - var _a, _b; - const { webpackStats: webpackRawStats, success, emittedFiles = [] } = buildEvent; - if (!webpackRawStats) { - throw new Error('Webpack stats build result is required.'); - } - // Fix incorrectly set `initial` value on chunks. - const extraEntryPoints = webpack_configs_1.normalizeExtraEntryPoints(options.styles || [], 'styles') - .concat(webpack_configs_1.normalizeExtraEntryPoints(options.scripts || [], 'scripts')); - const webpackStats = { - ...webpackRawStats, - chunks: async_chunks_1.markAsyncChunksNonInitial(webpackRawStats, extraEntryPoints), - }; - if (!success && useBundleDownleveling) { - // If using bundle downleveling then there is only one build - // If it fails show any diagnostic messages and bail - if (webpackStats && webpackStats.warnings.length > 0) { - context.logger.warn(stats_1.statsWarningsToString(webpackStats, { colors: true })); - } - if (webpackStats && webpackStats.errors.length > 0) { - context.logger.error(stats_1.statsErrorsToString(webpackStats, { colors: true })); - } - return { success }; - } - else if (success) { - outputPaths = output_paths_1.ensureOutputPaths(baseOutputPath, i18n); - let noModuleFiles; - let moduleFiles; - let files; - const scriptsEntryPointName = webpack_configs_1.normalizeExtraEntryPoints(options.scripts || [], 'scripts').map(x => x.bundleName); - if (isDifferentialLoadingNeeded && options.watch) { - moduleFiles = emittedFiles; - files = moduleFiles.filter(x => x.extension === '.css' || (x.name && scriptsEntryPointName.includes(x.name))); - if (i18n.shouldInline) { - const success = await i18n_inlining_1.i18nInlineEmittedFiles(context, emittedFiles, i18n, baseOutputPath, Array.from(outputPaths.values()), scriptsEntryPointName, - // tslint:disable-next-line: no-non-null-assertion - webpackStats.outputPath, target <= typescript_1.ScriptTarget.ES5, options.i18nMissingTranslation); - if (!success) { - return { success: false }; - } - } - } - else if (isDifferentialLoadingNeeded) { - moduleFiles = []; - noModuleFiles = []; - // Common options for all bundle process actions - const sourceMapOptions = utils_1.normalizeSourceMaps(options.sourceMap || false); - const actionOptions = { - optimize: utils_1.normalizeOptimization(options.optimization).scripts, - sourceMaps: sourceMapOptions.scripts, - hiddenSourceMaps: sourceMapOptions.hidden, - vendorSourceMaps: sourceMapOptions.vendor, - integrityAlgorithm: options.subresourceIntegrity ? 'sha384' : undefined, - }; - let mainChunkId; - const actions = []; - let workerReplacements; - const seen = new Set(); - for (const file of emittedFiles) { - // Assets are not processed nor injected into the index - if (file.asset) { - // WorkerPlugin adds worker files to assets - if (file.file.endsWith('.worker.js')) { - if (!workerReplacements) { - workerReplacements = []; - } - workerReplacements.push([ - file.file, - file.file.replace(/\-(es20\d{2}|esnext)/, '-es5'), - ]); - } - else { - continue; - } - } - // Scripts and non-javascript files are not processed - if (file.extension !== '.js' || - (file.name && scriptsEntryPointName.includes(file.name))) { - if (files === undefined) { - files = []; - } - files.push(file); - continue; - } - // Ignore already processed files; emittedFiles can contain duplicates - if (seen.has(file.file)) { - continue; - } - seen.add(file.file); - if (file.name === 'vendor' || (!mainChunkId && file.name === 'main')) { - // tslint:disable-next-line: no-non-null-assertion - mainChunkId = file.id.toString(); - } - // All files at this point except ES5 polyfills are module scripts - const es5Polyfills = file.file.startsWith('polyfills-es5'); - if (!es5Polyfills) { - moduleFiles.push(file); - } - // Retrieve the content/map for the file - // NOTE: Additional future optimizations will read directly from memory - // tslint:disable-next-line: no-non-null-assertion - let filename = path.join(webpackStats.outputPath, file.file); - const code = fs.readFileSync(filename, 'utf8'); - let map; - if (actionOptions.sourceMaps) { - try { - map = fs.readFileSync(filename + '.map', 'utf8'); - if (es5Polyfills) { - fs.unlinkSync(filename + '.map'); - } - } - catch (_c) { } - } - if (es5Polyfills) { - fs.unlinkSync(filename); - filename = filename.replace(/\-es20\d{2}/, ''); - } - const es2015Polyfills = file.file.startsWith('polyfills-es20'); - // Record the bundle processing action - // The runtime chunk gets special processing for lazy loaded files - actions.push({ - ...actionOptions, - filename, - code, - map, - // id is always present for non-assets - // tslint:disable-next-line: no-non-null-assertion - name: file.id, - runtime: file.file.startsWith('runtime'), - ignoreOriginal: es5Polyfills, - optimizeOnly: es2015Polyfills, - }); - // ES2015 polyfills are only optimized; optimization check was performed above - if (es2015Polyfills) { - continue; - } - // Add the newly created ES5 bundles to the index as nomodule scripts - const newFilename = es5Polyfills - ? file.file.replace(/\-es20\d{2}/, '') - : file.file.replace(/\-(es20\d{2}|esnext)/, '-es5'); - noModuleFiles.push({ ...file, file: newFilename }); - } - const processActions = []; - let processRuntimeAction; - const processResults = []; - for (const action of actions) { - // If SRI is enabled always process the runtime bundle - // Lazy route integrity values are stored in the runtime bundle - if (action.integrityAlgorithm && action.runtime) { - processRuntimeAction = action; - } - else { - processActions.push({ replacements: workerReplacements, ...action }); - } - } - const executor = new action_executor_1.BundleActionExecutor({ cachePath: cacheDownlevelPath, i18n }, options.subresourceIntegrity ? 'sha384' : undefined); - // Execute the bundle processing actions - try { - context.logger.info('Generating ES5 bundles for differential loading...'); - for await (const result of executor.processAll(processActions)) { - processResults.push(result); - } - // Runtime must be processed after all other files - if (processRuntimeAction) { - const runtimeOptions = { - ...processRuntimeAction, - runtimeData: processResults, - supportedBrowsers: buildBrowserFeatures.supportedBrowsers, - }; - processResults.push(await Promise.resolve().then(() => require('../utils/process-bundle')).then(m => m.process(runtimeOptions))); - } - context.logger.info('ES5 bundle generation complete.'); - if (i18n.shouldInline) { - context.logger.info('Generating localized bundles...'); - const inlineActions = []; - const processedFiles = new Set(); - for (const result of processResults) { - if (result.original) { - inlineActions.push({ - filename: path.basename(result.original.filename), - code: fs.readFileSync(result.original.filename, 'utf8'), - map: result.original.map && - fs.readFileSync(result.original.map.filename, 'utf8'), - outputPath: baseOutputPath, - es5: false, - missingTranslation: options.i18nMissingTranslation, - setLocale: result.name === mainChunkId, - }); - processedFiles.add(result.original.filename); - if (result.original.map) { - processedFiles.add(result.original.map.filename); - } - } - if (result.downlevel) { - inlineActions.push({ - filename: path.basename(result.downlevel.filename), - code: fs.readFileSync(result.downlevel.filename, 'utf8'), - map: result.downlevel.map && - fs.readFileSync(result.downlevel.map.filename, 'utf8'), - outputPath: baseOutputPath, - es5: true, - missingTranslation: options.i18nMissingTranslation, - setLocale: result.name === mainChunkId, - }); - processedFiles.add(result.downlevel.filename); - if (result.downlevel.map) { - processedFiles.add(result.downlevel.map.filename); - } - } - } - let hasErrors = false; - try { - for await (const result of executor.inlineAll(inlineActions)) { - if (options.verbose) { - context.logger.info(`Localized "${result.file}" [${result.count} translation(s)].`); - } - for (const diagnostic of result.diagnostics) { - if (diagnostic.type === 'error') { - hasErrors = true; - context.logger.error(diagnostic.message); - } - else { - context.logger.warn(diagnostic.message); - } - } - } - // Copy any non-processed files into the output locations - await copy_assets_1.copyAssets([ - { - glob: '**/*', - // tslint:disable-next-line: no-non-null-assertion - input: webpackStats.outputPath, - output: '', - ignore: [...processedFiles].map(f => - // tslint:disable-next-line: no-non-null-assertion - path.relative(webpackStats.outputPath, f)), - }, - ], Array.from(outputPaths.values()), ''); - } - catch (err) { - context.logger.error('Localized bundle generation failed: ' + err.message); - return { success: false }; - } - context.logger.info(`Localized bundle generation ${hasErrors ? 'failed' : 'complete'}.`); - if (hasErrors) { - return { success: false }; - } - } - } - finally { - executor.stop(); - } - // Copy assets - if (options.assets) { - try { - await copy_assets_1.copyAssets(utils_1.normalizeAssetPatterns(options.assets, new core_1.virtualFs.SyncDelegateHost(host), root, core_1.normalize(projectRoot), projectSourceRoot === undefined ? undefined : core_1.normalize(projectSourceRoot)), Array.from(outputPaths.values()), context.workspaceRoot); - } - catch (err) { - context.logger.error('Unable to copy assets: ' + err.message); - return { success: false }; - } - } - function generateBundleInfoStats(id, bundle, chunk) { - return stats_1.generateBundleStats({ - id, - size: bundle.size, - files: bundle.map ? [bundle.filename, bundle.map.filename] : [bundle.filename], - names: chunk && chunk.names, - entry: !!chunk && chunk.names.includes('runtime'), - initial: !!chunk && chunk.initial, - rendered: true, - }, true); - } - let bundleInfoText = ''; - for (const result of processResults) { - const chunk = webpackStats.chunks - && webpackStats.chunks.find((chunk) => chunk.id.toString() === result.name); - if (result.original) { - bundleInfoText += - '\n' + generateBundleInfoStats(result.name, result.original, chunk); - } - if (result.downlevel) { - bundleInfoText += - '\n' + generateBundleInfoStats(result.name, result.downlevel, chunk); - } - } - const unprocessedChunks = webpackStats.chunks && webpackStats.chunks - .filter((chunk) => !processResults - .find((result) => chunk.id.toString() === result.name)) || []; - for (const chunk of unprocessedChunks) { - const asset = webpackStats.assets && webpackStats.assets.find(a => a.name === chunk.files[0]); - bundleInfoText += - '\n' + stats_1.generateBundleStats({ ...chunk, size: asset && asset.size }, true); - } - bundleInfoText += - '\n' + - stats_1.generateBuildStats((webpackStats && webpackStats.hash) || '', Date.now() - startTime, true); - context.logger.info(bundleInfoText); - // Check for budget errors and display them to the user. - const budgets = options.budgets || []; - const budgetFailures = bundle_calculator_1.checkBudgets(budgets, webpackStats, processResults); - for (const { severity, message } of budgetFailures) { - const msg = `budgets: ${message}`; - switch (severity) { - case bundle_calculator_1.ThresholdSeverity.Warning: - webpackStats.warnings.push(msg); - break; - case bundle_calculator_1.ThresholdSeverity.Error: - webpackStats.errors.push(msg); - break; - default: - assertNever(severity); - break; - } - } - if (webpackStats && webpackStats.warnings.length > 0) { - context.logger.warn(stats_1.statsWarningsToString(webpackStats, { colors: true })); - } - if (webpackStats && webpackStats.errors.length > 0) { - context.logger.error(stats_1.statsErrorsToString(webpackStats, { colors: true })); - return { success: false }; - } - } - else { - files = emittedFiles.filter(x => x.name !== 'polyfills-es5'); - noModuleFiles = emittedFiles.filter(x => x.name === 'polyfills-es5'); - if (i18n.shouldInline) { - const success = await i18n_inlining_1.i18nInlineEmittedFiles(context, emittedFiles, i18n, baseOutputPath, Array.from(outputPaths.values()), scriptsEntryPointName, - // tslint:disable-next-line: no-non-null-assertion - webpackStats.outputPath, target <= typescript_1.ScriptTarget.ES5, options.i18nMissingTranslation); - if (!success) { - return { success: false }; - } - } - } - if (options.index) { - for (const [locale, outputPath] of outputPaths.entries()) { - let localeBaseHref; - if (i18n.locales[locale] && i18n.locales[locale].baseHref !== '') { - localeBaseHref = utils_1.urlJoin(options.baseHref || '', (_a = i18n.locales[locale].baseHref) !== null && _a !== void 0 ? _a : `/${locale}/`); - } - try { - await generateIndex(outputPath, options, root, files, noModuleFiles, moduleFiles, transforms.indexHtml, - // i18nLocale is used when Ivy is disabled - locale || options.i18nLocale, localeBaseHref || options.baseHref); - } - catch (err) { - return { success: false, error: mapErrorToMessage(err) }; - } - } - } - if (!options.watch && options.serviceWorker) { - for (const [locale, outputPath] of outputPaths.entries()) { - let localeBaseHref; - if (i18n.locales[locale] && i18n.locales[locale].baseHref !== '') { - localeBaseHref = utils_1.urlJoin(options.baseHref || '', (_b = i18n.locales[locale].baseHref) !== null && _b !== void 0 ? _b : `/${locale}/`); - } - try { - await service_worker_1.augmentAppWithServiceWorker(host, root, core_1.normalize(projectRoot), core_1.normalize(outputPath), localeBaseHref || options.baseHref || '/', options.ngswConfigPath); - } - catch (err) { - return { success: false, error: mapErrorToMessage(err) }; - } - } - } - } - return { success }; - }), operators_1.map(event => ({ - ...event, - baseOutputPath, - outputPath: baseOutputPath, - outputPaths: outputPaths && Array.from(outputPaths.values()) || [baseOutputPath], - }))); - })); -} -exports.buildWebpackBrowser = buildWebpackBrowser; -function generateIndex(baseOutputPath, options, root, files, noModuleFiles, moduleFiles, transformer, locale, baseHref) { - const host = new node_1.NodeJsSyncHost(); - return write_index_html_1.writeIndexHtml({ - host, - outputPath: core_1.join(core_1.normalize(baseOutputPath), webpack_browser_config_1.getIndexOutputFile(options)), - indexPath: core_1.join(core_1.normalize(root), webpack_browser_config_1.getIndexInputFile(options)), - files, - noModuleFiles, - moduleFiles, - baseHref, - deployUrl: options.deployUrl, - sri: options.subresourceIntegrity, - scripts: options.scripts, - styles: options.styles, - postTransform: transformer, - crossOrigin: options.crossOrigin, - lang: locale, - }).toPromise(); -} -function mapErrorToMessage(error) { - if (error instanceof Error) { - return error.message; - } - if (typeof error === 'string') { - return error; - } - return undefined; -} -function assertNever(input) { - throw new Error(`Unexpected call to assertNever() with input: ${JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`); -} -exports.default = architect_1.createBuilder(buildWebpackBrowser); diff --git a/src/builders/app-shell/index.d.ts b/src/builders/app-shell/index.d.ts new file mode 100644 index 000000000..71f096491 --- /dev/null +++ b/src/builders/app-shell/index.d.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { JsonObject } from '@angular-devkit/core'; +import { Schema as BuildWebpackAppShellSchema } from './schema'; +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; diff --git a/src/builders/app-shell/index.js b/src/builders/app-shell/index.js new file mode 100644 index 000000000..0a177a3ef --- /dev/null +++ b/src/builders/app-shell/index.js @@ -0,0 +1,192 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const private_1 = require("@angular/build/private"); +const architect_1 = require("@angular-devkit/architect"); +const fs = __importStar(require("node:fs")); +const path = __importStar(require("node:path")); +const piscina_1 = __importDefault(require("piscina")); +const utils_1 = require("../../utils"); +const error_1 = require("../../utils/error"); +const spinner_1 = require("../../utils/spinner"); +async function _renderUniversal(options, context, browserResult, serverResult, spinner) { + // Get browser target options. + const browserTarget = (0, architect_1.targetFromTargetString)(options.browserTarget); + const rawBrowserOptions = await context.getTargetOptions(browserTarget); + const browserBuilderName = await context.getBuilderNameForTarget(browserTarget); + const browserOptions = await context.validateOptions(rawBrowserOptions, browserBuilderName); + // Locate zone.js to load in the render worker + const root = context.workspaceRoot; + let zonePackage; + try { + zonePackage = require.resolve('zone.js', { paths: [root] }); + } + catch { } + const projectName = context.target && context.target.project; + if (!projectName) { + throw new Error('The builder requires a target.'); + } + const projectMetadata = await context.getProjectMetadata(projectName); + const projectRoot = path.join(root, projectMetadata.root ?? ''); + const { styles } = (0, utils_1.normalizeOptimization)(browserOptions.optimization); + let inlineCriticalCssProcessor; + if (styles.inlineCritical) { + const { InlineCriticalCssProcessor } = await Promise.resolve().then(() => __importStar(require('@angular/build/private'))); + inlineCriticalCssProcessor = new InlineCriticalCssProcessor({ + minify: styles.minify, + deployUrl: browserOptions.deployUrl, + }); + } + const renderWorker = new piscina_1.default({ + filename: require.resolve('./render-worker'), + maxThreads: 1, + workerData: { zonePackage }, + recordTiming: false, + }); + try { + for (const { path: outputPath, baseHref } of browserResult.outputs) { + const localeDirectory = path.relative(browserResult.baseOutputPath, outputPath); + const browserIndexOutputPath = path.join(outputPath, 'index.html'); + const indexHtml = await fs.promises.readFile(browserIndexOutputPath, 'utf8'); + const serverBundlePath = await _getServerModuleBundlePath(options, context, serverResult, localeDirectory); + let html = await renderWorker.run({ + serverBundlePath, + document: indexHtml, + url: options.route, + }); + // Overwrite the client index file. + const outputIndexPath = options.outputIndexPath + ? path.join(root, options.outputIndexPath) + : browserIndexOutputPath; + if (inlineCriticalCssProcessor) { + const { content, warnings, errors } = await inlineCriticalCssProcessor.process(html, { + outputPath, + }); + html = content; + if (warnings.length || errors.length) { + spinner.stop(); + warnings.forEach((m) => context.logger.warn(m)); + errors.forEach((m) => context.logger.error(m)); + spinner.start(); + } + } + await fs.promises.writeFile(outputIndexPath, html); + if (browserOptions.serviceWorker) { + await (0, private_1.augmentAppWithServiceWorker)(projectRoot, root, outputPath, baseHref ?? '/', browserOptions.ngswConfigPath); + } + } + } + finally { + await renderWorker.destroy(); + } + return browserResult; +} +async function _getServerModuleBundlePath(options, context, serverResult, browserLocaleDirectory) { + if (options.appModuleBundle) { + return path.join(context.workspaceRoot, options.appModuleBundle); + } + const { baseOutputPath = '' } = serverResult; + const outputPath = path.join(baseOutputPath, browserLocaleDirectory); + if (!fs.existsSync(outputPath)) { + throw new Error(`Could not find server output directory: ${outputPath}.`); + } + const re = /^main\.(?:[a-zA-Z0-9]{16}\.)?js$/; + const maybeMain = fs.readdirSync(outputPath).find((x) => re.test(x)); + if (!maybeMain) { + throw new Error('Could not find the main bundle.'); + } + return path.join(outputPath, maybeMain); +} +async function _appShellBuilder(options, context) { + const browserTarget = (0, architect_1.targetFromTargetString)(options.browserTarget); + const serverTarget = (0, architect_1.targetFromTargetString)(options.serverTarget); + // Never run the browser target in watch mode. + // If service worker is needed, it will be added in _renderUniversal(); + const browserOptions = (await context.getTargetOptions(browserTarget)); + const optimization = (0, utils_1.normalizeOptimization)(browserOptions.optimization); + optimization.styles.inlineCritical = false; + const browserTargetRun = await context.scheduleTarget(browserTarget, { + watch: false, + serviceWorker: false, + optimization: optimization, + }); + if (browserTargetRun.info.builderName === '@angular-devkit/build-angular:application') { + return { + success: false, + error: '"@angular-devkit/build-angular:application" has built-in app-shell generation capabilities. ' + + 'The "appShell" option should be used instead.', + }; + } + const serverTargetRun = await context.scheduleTarget(serverTarget, { + watch: false, + }); + let spinner; + try { + const [browserResult, serverResult] = await Promise.all([ + browserTargetRun.result, + serverTargetRun.result, + ]); + if (browserResult.success === false || browserResult.baseOutputPath === undefined) { + return browserResult; + } + else if (serverResult.success === false) { + return serverResult; + } + spinner = new spinner_1.Spinner(); + spinner.start('Generating application shell...'); + const result = await _renderUniversal(options, context, browserResult, serverResult, spinner); + spinner.succeed('Application shell generation complete.'); + return result; + } + catch (err) { + spinner?.fail('Application shell generation failed.'); + (0, error_1.assertIsError)(err); + return { success: false, error: err.message }; + } + finally { + await Promise.all([browserTargetRun.stop(), serverTargetRun.stop()]); + } +} +exports.default = (0, architect_1.createBuilder)(_appShellBuilder); diff --git a/src/builders/app-shell/render-worker.d.ts b/src/builders/app-shell/render-worker.d.ts new file mode 100644 index 000000000..7ed6787f6 --- /dev/null +++ b/src/builders/app-shell/render-worker.d.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +/** + * A request to render a Server bundle generate by the universal server builder. + */ +interface RenderRequest { + /** + * The path to the server bundle that should be loaded and rendered. + */ + serverBundlePath: string; + /** + * The existing HTML document as a string that will be augmented with the rendered application. + */ + document: string; + /** + * An optional URL path that represents the Angular route that should be rendered. + */ + url: string; +} +/** + * Renders an application based on a provided server bundle path, initial document, and optional URL route. + * @param param0 A request to render a server bundle. + * @returns A promise that resolves to the render HTML document for the application. + */ +declare function render({ serverBundlePath, document, url }: RenderRequest): Promise; +/** + * The default export will be the promise returned by the initialize function. + * This is awaited by piscina prior to using the Worker. + */ +declare const _default: Promise; +export default _default; diff --git a/src/builders/app-shell/render-worker.js b/src/builders/app-shell/render-worker.js new file mode 100644 index 000000000..5b7306134 --- /dev/null +++ b/src/builders/app-shell/render-worker.js @@ -0,0 +1,114 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_assert_1 = __importDefault(require("node:assert")); +const node_worker_threads_1 = require("node:worker_threads"); +/** + * The fully resolved path to the zone.js package that will be loaded during worker initialization. + * This is passed as workerData when setting up the worker via the `piscina` package. + */ +const { zonePackage } = node_worker_threads_1.workerData; +/** + * Renders an application based on a provided server bundle path, initial document, and optional URL route. + * @param param0 A request to render a server bundle. + * @returns A promise that resolves to the render HTML document for the application. + */ +async function render({ serverBundlePath, document, url }) { + const { ɵSERVER_CONTEXT, AppServerModule, renderModule, renderApplication, default: bootstrapAppFn, } = (await Promise.resolve(`${serverBundlePath}`).then(s => __importStar(require(s)))); + (0, node_assert_1.default)(ɵSERVER_CONTEXT, `ɵSERVER_CONTEXT was not exported from: ${serverBundlePath}.`); + const platformProviders = [ + { + provide: ɵSERVER_CONTEXT, + useValue: 'app-shell', + }, + ]; + let renderAppPromise; + // Render platform server module + if (isBootstrapFn(bootstrapAppFn)) { + (0, node_assert_1.default)(renderApplication, `renderApplication was not exported from: ${serverBundlePath}.`); + renderAppPromise = renderApplication(bootstrapAppFn, { + document, + url, + platformProviders, + }); + } + else { + (0, node_assert_1.default)(renderModule, `renderModule was not exported from: ${serverBundlePath}.`); + const moduleClass = bootstrapAppFn || AppServerModule; + (0, node_assert_1.default)(moduleClass, `Neither an AppServerModule nor a bootstrapping function was exported from: ${serverBundlePath}.`); + renderAppPromise = renderModule(moduleClass, { + document, + url, + extraProviders: platformProviders, + }); + } + // The below should really handled by the framework!!!. + // See: https://github.com/angular/angular/issues/51549 + let timer; + const renderingTimeout = new Promise((_, reject) => (timer = setTimeout(() => reject(new Error(`Page ${new URL(url, 'resolve://').pathname} did not render in 30 seconds.`)), 30_000))); + return Promise.race([renderAppPromise, renderingTimeout]).finally(() => clearTimeout(timer)); +} +function isBootstrapFn(value) { + // We can differentiate between a module and a bootstrap function by reading compiler-generated `ɵmod` static property: + return typeof value === 'function' && !('ɵmod' in value); +} +/** + * Initializes the worker when it is first created by loading the Zone.js package + * into the worker instance. + * + * @returns A promise resolving to the render function of the worker. + */ +async function initialize() { + if (zonePackage) { + await Promise.resolve(`${zonePackage}`).then(s => __importStar(require(s))); + } + // Return the render function for use + return render; +} +/** + * The default export will be the promise returned by the initialize function. + * This is awaited by piscina prior to using the Worker. + */ +exports.default = initialize(); diff --git a/src/app-shell/schema.d.ts b/src/builders/app-shell/schema.d.ts similarity index 59% rename from src/app-shell/schema.d.ts rename to src/builders/app-shell/schema.d.ts index 92e147e25..df022590a 100644 --- a/src/app-shell/schema.d.ts +++ b/src/builders/app-shell/schema.d.ts @@ -1,7 +1,7 @@ /** * App Shell target options for Build Facade. */ -export interface Schema { +export type Schema = { /** * Script that exports the Server AppModule to render. This should be the main JavaScript * outputted by the server target. By default we will resolve the outputPath of the @@ -9,7 +9,9 @@ export interface Schema { */ appModuleBundle?: string; /** - * Target to build. + * A browser builder target use for rendering the application shell in the format of + * `project:target[:configuration]`. You can also pass in more than one configuration name + * as a comma-separated list. Example: `project:target:production,staging`. */ browserTarget: string; /** @@ -26,7 +28,9 @@ export interface Schema { */ route?: string; /** - * Server target to use for rendering the app shell. + * A server builder target use for rendering the application shell in the format of + * `project:target[:configuration]`. You can also pass in more than one configuration name + * as a comma-separated list. Example: `project:target:production,staging`. */ serverTarget: string; -} +}; diff --git a/src/app-shell/schema.js b/src/builders/app-shell/schema.js similarity index 100% rename from src/app-shell/schema.js rename to src/builders/app-shell/schema.js diff --git a/src/app-shell/schema.json b/src/builders/app-shell/schema.json similarity index 67% rename from src/app-shell/schema.json rename to src/builders/app-shell/schema.json index b89b79526..027cc9f8a 100644 --- a/src/app-shell/schema.json +++ b/src/builders/app-shell/schema.json @@ -6,12 +6,12 @@ "properties": { "browserTarget": { "type": "string", - "description": "Target to build.", + "description": "A browser builder target use for rendering the application shell in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" }, "serverTarget": { "type": "string", - "description": "Server target to use for rendering the app shell.", + "description": "A server builder target use for rendering the application shell in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" }, "appModuleBundle": { @@ -33,8 +33,5 @@ } }, "additionalProperties": false, - "required": [ - "browserTarget", - "serverTarget" - ] + "required": ["browserTarget", "serverTarget"] } diff --git a/src/builders/browser-esbuild/index.d.ts b/src/builders/browser-esbuild/index.d.ts new file mode 100644 index 000000000..ea4b5a6c4 --- /dev/null +++ b/src/builders/browser-esbuild/index.d.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { type ApplicationBuilderOptions } from '@angular/build'; +import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import type { Plugin } from 'esbuild'; +import type { Schema as BrowserBuilderOptions } from './schema'; +export type { BrowserBuilderOptions }; +type OutputPathClass = Exclude; +/** + * Main execution function for the esbuild-based application builder. + * The options are compatible with the Webpack-based builder. + * @param userOptions The browser builder options to use when setting up the application build + * @param context The Architect builder context object + * @returns An async iterable with the builder result output + */ +export declare function buildEsbuildBrowser(userOptions: BrowserBuilderOptions, context: BuilderContext, infrastructureSettings?: { + write?: boolean; +}, plugins?: Plugin[]): AsyncIterable; +export declare function convertBrowserOptions(options: BrowserBuilderOptions): Omit & { + outputPath: OutputPathClass; +}; +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; diff --git a/src/builders/browser-esbuild/index.js b/src/builders/browser-esbuild/index.js new file mode 100644 index 000000000..bd39f3c91 --- /dev/null +++ b/src/builders/browser-esbuild/index.js @@ -0,0 +1,52 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.buildEsbuildBrowser = buildEsbuildBrowser; +exports.convertBrowserOptions = convertBrowserOptions; +const build_1 = require("@angular/build"); +const architect_1 = require("@angular-devkit/architect"); +/** + * Main execution function for the esbuild-based application builder. + * The options are compatible with the Webpack-based builder. + * @param userOptions The browser builder options to use when setting up the application build + * @param context The Architect builder context object + * @returns An async iterable with the builder result output + */ +async function* buildEsbuildBrowser(userOptions, context, infrastructureSettings, plugins) { + // Warn about any unsupported options + if (userOptions['vendorChunk']) { + context.logger.warn(`The 'vendorChunk' option is not used by this builder and will be ignored.`); + } + if (userOptions['commonChunk'] === false) { + context.logger.warn(`The 'commonChunk' option is always enabled by this builder and will be ignored.`); + } + if (userOptions['webWorkerTsConfig']) { + context.logger.warn(`The 'webWorkerTsConfig' option is not yet supported by this builder.`); + } + // Convert browser builder options to application builder options + const normalizedOptions = convertBrowserOptions(userOptions); + // Execute the application builder + yield* (0, build_1.buildApplication)(normalizedOptions, context, { codePlugins: plugins }); +} +function convertBrowserOptions(options) { + const { main: browser, outputPath, ngswConfigPath, serviceWorker, polyfills, resourcesOutputPath, ...otherOptions } = options; + return { + browser, + serviceWorker: serviceWorker ? ngswConfigPath : false, + polyfills: typeof polyfills === 'string' ? [polyfills] : polyfills, + outputPath: { + base: outputPath, + browser: '', + server: '', + media: resourcesOutputPath ?? 'media', + }, + ...otherOptions, + }; +} +exports.default = (0, architect_1.createBuilder)(buildEsbuildBrowser); diff --git a/src/builders/browser-esbuild/schema.d.ts b/src/builders/browser-esbuild/schema.d.ts new file mode 100644 index 000000000..b960c4496 --- /dev/null +++ b/src/builders/browser-esbuild/schema.d.ts @@ -0,0 +1,431 @@ +/** + * Browser target options + */ +export type Schema = { + /** + * A list of CommonJS or AMD packages that are allowed to be used without a build time + * warning. Use `'*'` to allow all. + */ + allowedCommonJsDependencies?: string[]; + /** + * Build using Ahead of Time compilation. + */ + aot?: boolean; + /** + * List of static application assets. + */ + assets?: AssetPattern[]; + /** + * Base url for the application being built. + */ + baseHref?: string; + /** + * Budget thresholds to ensure parts of your application stay within boundaries which you + * set. + */ + budgets?: Budget[]; + /** + * Enables advanced build optimizations when using the 'aot' option. + */ + buildOptimizer?: boolean; + /** + * Generate a seperate bundle containing code used across multiple bundles. + */ + commonChunk?: boolean; + /** + * Define the crossorigin attribute setting of elements that provide CORS support. + */ + crossOrigin?: CrossOrigin; + /** + * Delete the output path before building. + */ + deleteOutputPath?: boolean; + /** + * Customize the base path for the URLs of resources in 'index.html' and component + * stylesheets. This option is only necessary for specific deployment scenarios, such as + * with Angular Elements or when utilizing different CDN locations. + */ + deployUrl?: string; + /** + * Exclude the listed external dependencies from being bundled into the bundle. Instead, the + * created bundle relies on these dependencies to be available during runtime. + */ + externalDependencies?: string[]; + /** + * Extract all licenses in a separate file. + */ + extractLicenses?: boolean; + /** + * Replace compilation source files with other compilation source files in the build. + */ + fileReplacements?: FileReplacement[]; + /** + * How to handle duplicate translations for i18n. + */ + i18nDuplicateTranslation?: I18NTranslation; + /** + * How to handle missing translations for i18n. + */ + i18nMissingTranslation?: I18NTranslation; + /** + * Configures the generation of the application's HTML index. + */ + index: IndexUnion; + /** + * The stylesheet language to use for the application's inline component styles. + */ + inlineStyleLanguage?: InlineStyleLanguage; + /** + * Translate the bundles in one or more locales. + */ + localize?: Localize; + /** + * The full path for the main entry point to the app, relative to the current workspace. + */ + main: string; + /** + * Use file name for lazy loaded chunks. + */ + namedChunks?: boolean; + /** + * Path to ngsw-config.json. + */ + ngswConfigPath?: string; + /** + * Enables optimization of the build output. Including minification of scripts and styles, + * tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For + * more information, see + * https://angular.dev/reference/configs/workspace-config#optimization-configuration. + */ + optimization?: OptimizationUnion; + /** + * Define the output filename cache-busting hashing mode. + */ + outputHashing?: OutputHashing; + /** + * The full path for the new output directory, relative to the current workspace. + */ + outputPath: string; + /** + * Enable and define the file watching poll time period in milliseconds. + */ + poll?: number; + /** + * Polyfills to be included in the build. + */ + polyfills?: Polyfills; + /** + * Do not use the real path when resolving modules. If unset then will default to `true` if + * NodeJS option --preserve-symlinks is set. + */ + preserveSymlinks?: boolean; + /** + * Log progress to the console while building. + */ + progress?: boolean; + /** + * The path where style resources will be placed, relative to outputPath. + */ + resourcesOutputPath?: string; + /** + * Global scripts to be included in the build. + */ + scripts?: ScriptElement[]; + /** + * Generates a service worker config for production builds. + */ + serviceWorker?: boolean; + /** + * Output source maps for scripts and styles. For more information, see + * https://angular.dev/reference/configs/workspace-config#source-map-configuration. + */ + sourceMap?: SourceMapUnion; + /** + * Generates a 'stats.json' file which can be analyzed using tools such as + * 'webpack-bundle-analyzer'. + */ + statsJson?: boolean; + /** + * Options to pass to style preprocessors. + */ + stylePreprocessorOptions?: StylePreprocessorOptions; + /** + * Global styles to be included in the build. + */ + styles?: StyleElement[]; + /** + * Enables the use of subresource integrity validation. + */ + subresourceIntegrity?: boolean; + /** + * The full path for the TypeScript configuration file, relative to the current workspace. + */ + tsConfig: string; + /** + * Generate a seperate bundle containing only vendor libraries. This option should only be + * used for development to reduce the incremental compilation time. + */ + vendorChunk?: boolean; + /** + * Adds more details to output logging. + */ + verbose?: boolean; + /** + * Run build when files change. + */ + watch?: boolean; + /** + * TypeScript configuration for Web Worker modules. + */ + webWorkerTsConfig?: string; +}; +export type AssetPattern = AssetPatternClass | string; +export type AssetPatternClass = { + /** + * Allow glob patterns to follow symlink directories. This allows subdirectories of the + * symlink to be searched. + */ + followSymlinks?: boolean; + /** + * The pattern to match. + */ + glob: string; + /** + * An array of globs to ignore. + */ + ignore?: string[]; + /** + * The input directory path in which to apply 'glob'. Defaults to the project root. + */ + input: string; + /** + * Absolute path within the output. + */ + output?: string; +}; +export type Budget = { + /** + * The baseline size for comparison. + */ + baseline?: string; + /** + * The threshold for error relative to the baseline (min & max). + */ + error?: string; + /** + * The maximum threshold for error relative to the baseline. + */ + maximumError?: string; + /** + * The maximum threshold for warning relative to the baseline. + */ + maximumWarning?: string; + /** + * The minimum threshold for error relative to the baseline. + */ + minimumError?: string; + /** + * The minimum threshold for warning relative to the baseline. + */ + minimumWarning?: string; + /** + * The name of the bundle. + */ + name?: string; + /** + * The type of budget. + */ + type: Type; + /** + * The threshold for warning relative to the baseline (min & max). + */ + warning?: string; +}; +/** + * The type of budget. + */ +export declare enum Type { + All = "all", + AllScript = "allScript", + Any = "any", + AnyComponentStyle = "anyComponentStyle", + AnyScript = "anyScript", + Bundle = "bundle", + Initial = "initial" +} +/** + * Define the crossorigin attribute setting of elements that provide CORS support. + */ +export declare enum CrossOrigin { + Anonymous = "anonymous", + None = "none", + UseCredentials = "use-credentials" +} +export type FileReplacement = { + replace: string; + with: string; +}; +/** + * How to handle duplicate translations for i18n. + * + * How to handle missing translations for i18n. + */ +export declare enum I18NTranslation { + Error = "error", + Ignore = "ignore", + Warning = "warning" +} +/** + * Configures the generation of the application's HTML index. + */ +export type IndexUnion = boolean | IndexObject | string; +export type IndexObject = { + /** + * The path of a file to use for the application's generated HTML index. + */ + input: string; + /** + * 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. + */ + output?: string; + [property: string]: any; +}; +/** + * The stylesheet language to use for the application's inline component styles. + */ +export declare enum InlineStyleLanguage { + Css = "css", + Less = "less", + Sass = "sass", + Scss = "scss" +} +/** + * Translate the bundles in one or more locales. + */ +export type Localize = string[] | boolean; +/** + * Enables optimization of the build output. Including minification of scripts and styles, + * tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For + * more information, see + * https://angular.dev/reference/configs/workspace-config#optimization-configuration. + */ +export type OptimizationUnion = boolean | OptimizationClass; +export type OptimizationClass = { + /** + * Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` + * environment variable can be used to specify a proxy server. + */ + fonts?: FontsUnion; + /** + * Enables optimization of the scripts output. + */ + scripts?: boolean; + /** + * Enables optimization of the styles output. + */ + styles?: StylesUnion; +}; +/** + * Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` + * environment variable can be used to specify a proxy server. + */ +export type FontsUnion = boolean | FontsClass; +export type FontsClass = { + /** + * Reduce render blocking requests by inlining external Google Fonts and Adobe Fonts CSS + * definitions in the application's HTML index file. This option requires internet access. + * `HTTPS_PROXY` environment variable can be used to specify a proxy server. + */ + inline?: boolean; +}; +/** + * Enables optimization of the styles output. + */ +export type StylesUnion = boolean | StylesClass; +export type StylesClass = { + /** + * Extract and inline critical CSS definitions to improve first paint time. + */ + inlineCritical?: boolean; + /** + * Minify CSS definitions by removing extraneous whitespace and comments, merging + * identifiers and minimizing values. + */ + minify?: boolean; +}; +/** + * Define the output filename cache-busting hashing mode. + */ +export declare enum OutputHashing { + All = "all", + Bundles = "bundles", + Media = "media", + None = "none" +} +/** + * Polyfills to be included in the build. + */ +export type Polyfills = string[] | string; +export type ScriptElement = ScriptClass | string; +export type ScriptClass = { + /** + * The bundle name for this extra entry point. + */ + bundleName?: string; + /** + * If the bundle will be referenced in the HTML file. + */ + inject?: boolean; + /** + * The file to include. + */ + input: string; +}; +/** + * Output source maps for scripts and styles. For more information, see + * https://angular.dev/reference/configs/workspace-config#source-map-configuration. + */ +export type SourceMapUnion = boolean | SourceMapClass; +export type SourceMapClass = { + /** + * Output source maps used for error reporting tools. + */ + hidden?: boolean; + /** + * Output source maps for all scripts. + */ + scripts?: boolean; + /** + * Output source maps for all styles. + */ + styles?: boolean; + /** + * Resolve vendor packages source maps. + */ + vendor?: boolean; +}; +/** + * Options to pass to style preprocessors. + */ +export type StylePreprocessorOptions = { + /** + * Paths to include. Paths will be resolved to workspace root. + */ + includePaths?: string[]; +}; +export type StyleElement = StyleClass | string; +export type StyleClass = { + /** + * The bundle name for this extra entry point. + */ + bundleName?: string; + /** + * If the bundle will be referenced in the HTML file. + */ + inject?: boolean; + /** + * The file to include. + */ + input: string; +}; diff --git a/src/browser/schema.js b/src/builders/browser-esbuild/schema.js similarity index 53% rename from src/browser/schema.js rename to src/builders/browser-esbuild/schema.js index 6262bdcf7..b721d8278 100644 --- a/src/browser/schema.js +++ b/src/builders/browser-esbuild/schema.js @@ -2,7 +2,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE // CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). Object.defineProperty(exports, "__esModule", { value: true }); -exports.OutputHashing = exports.I18NMissingTranslation = exports.CrossOrigin = exports.Type = void 0; +exports.OutputHashing = exports.InlineStyleLanguage = exports.I18NTranslation = exports.CrossOrigin = exports.Type = void 0; /** * The type of budget. */ @@ -15,7 +15,7 @@ var Type; Type["AnyScript"] = "anyScript"; Type["Bundle"] = "bundle"; Type["Initial"] = "initial"; -})(Type = exports.Type || (exports.Type = {})); +})(Type || (exports.Type = Type = {})); /** * Define the crossorigin attribute setting of elements that provide CORS support. */ @@ -24,16 +24,28 @@ var CrossOrigin; CrossOrigin["Anonymous"] = "anonymous"; CrossOrigin["None"] = "none"; CrossOrigin["UseCredentials"] = "use-credentials"; -})(CrossOrigin = exports.CrossOrigin || (exports.CrossOrigin = {})); +})(CrossOrigin || (exports.CrossOrigin = CrossOrigin = {})); /** + * How to handle duplicate translations for i18n. + * * How to handle missing translations for i18n. */ -var I18NMissingTranslation; -(function (I18NMissingTranslation) { - I18NMissingTranslation["Error"] = "error"; - I18NMissingTranslation["Ignore"] = "ignore"; - I18NMissingTranslation["Warning"] = "warning"; -})(I18NMissingTranslation = exports.I18NMissingTranslation || (exports.I18NMissingTranslation = {})); +var I18NTranslation; +(function (I18NTranslation) { + I18NTranslation["Error"] = "error"; + I18NTranslation["Ignore"] = "ignore"; + I18NTranslation["Warning"] = "warning"; +})(I18NTranslation || (exports.I18NTranslation = I18NTranslation = {})); +/** + * The stylesheet language to use for the application's inline component styles. + */ +var InlineStyleLanguage; +(function (InlineStyleLanguage) { + InlineStyleLanguage["Css"] = "css"; + InlineStyleLanguage["Less"] = "less"; + InlineStyleLanguage["Sass"] = "sass"; + InlineStyleLanguage["Scss"] = "scss"; +})(InlineStyleLanguage || (exports.InlineStyleLanguage = InlineStyleLanguage = {})); /** * Define the output filename cache-busting hashing mode. */ @@ -43,4 +55,4 @@ var OutputHashing; OutputHashing["Bundles"] = "bundles"; OutputHashing["Media"] = "media"; OutputHashing["None"] = "none"; -})(OutputHashing = exports.OutputHashing || (exports.OutputHashing = {})); +})(OutputHashing || (exports.OutputHashing = OutputHashing = {})); diff --git a/src/builders/browser-esbuild/schema.json b/src/builders/browser-esbuild/schema.json new file mode 100644 index 000000000..edb91222d --- /dev/null +++ b/src/builders/browser-esbuild/schema.json @@ -0,0 +1,541 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Esbuild browser schema for Build Facade.", + "description": "Browser target options", + "type": "object", + "properties": { + "assets": { + "type": "array", + "description": "List of static application assets.", + "default": [], + "items": { + "$ref": "#/definitions/assetPattern" + } + }, + "main": { + "type": "string", + "description": "The full path for the main entry point to the app, relative to the current workspace." + }, + "polyfills": { + "description": "Polyfills to be included in the build.", + "oneOf": [ + { + "type": "array", + "description": "A list of polyfills to include in the build. Can be a full path for a file, relative to the current workspace or module specifier. Example: 'zone.js'.", + "items": { + "type": "string", + "uniqueItems": true + }, + "default": [] + }, + { + "type": "string", + "description": "The full path for the polyfills file, relative to the current workspace or a module specifier. Example: 'zone.js'." + } + ] + }, + "tsConfig": { + "type": "string", + "description": "The full path for the TypeScript configuration file, relative to the current workspace." + }, + "scripts": { + "description": "Global scripts to be included in the build.", + "type": "array", + "default": [], + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include.", + "pattern": "\\.[cm]?jsx?$" + }, + "bundleName": { + "type": "string", + "pattern": "^[\\w\\-.]*$", + "description": "The bundle name for this extra entry point." + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true + } + }, + "additionalProperties": false, + "required": ["input"] + }, + { + "type": "string", + "description": "The JavaScript/TypeScript file or package containing the file to include." + } + ] + } + }, + "styles": { + "description": "Global styles to be included in the build.", + "type": "array", + "default": [], + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include.", + "pattern": "\\.(?:css|scss|sass|less)$" + }, + "bundleName": { + "type": "string", + "pattern": "^[\\w\\-.]*$", + "description": "The bundle name for this extra entry point." + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true + } + }, + "additionalProperties": false, + "required": ["input"] + }, + { + "type": "string", + "description": "The file to include.", + "pattern": "\\.(?:css|scss|sass|less)$" + } + ] + } + }, + "inlineStyleLanguage": { + "description": "The stylesheet language to use for the application's inline component styles.", + "type": "string", + "default": "css", + "enum": ["css", "less", "sass", "scss"] + }, + "stylePreprocessorOptions": { + "description": "Options to pass to style preprocessors.", + "type": "object", + "properties": { + "includePaths": { + "description": "Paths to include. Paths will be resolved to workspace root.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + } + }, + "additionalProperties": false + }, + "externalDependencies": { + "description": "Exclude the listed external dependencies from being bundled into the bundle. Instead, the created bundle relies on these dependencies to be available during runtime.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "optimization": { + "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For more information, see https://angular.dev/reference/configs/workspace-config#optimization-configuration.", + "default": true, + "x-user-analytics": "ep.ng_optimization", + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Enables optimization of the scripts output.", + "default": true + }, + "styles": { + "description": "Enables optimization of the styles output.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "minify": { + "type": "boolean", + "description": "Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers and minimizing values.", + "default": true + }, + "inlineCritical": { + "type": "boolean", + "description": "Extract and inline critical CSS definitions to improve first paint time.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + }, + "fonts": { + "description": "Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "inline": { + "type": "boolean", + "description": "Reduce render blocking requests by inlining external Google Fonts and Adobe Fonts CSS definitions in the application's HTML index file. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + }, + "fileReplacements": { + "description": "Replace compilation source files with other compilation source files in the build.", + "type": "array", + "items": { + "$ref": "#/definitions/fileReplacement" + }, + "default": [] + }, + "outputPath": { + "type": "string", + "description": "The full path for the new output directory, relative to the current workspace." + }, + "resourcesOutputPath": { + "type": "string", + "description": "The path where style resources will be placed, relative to outputPath." + }, + "aot": { + "type": "boolean", + "description": "Build using Ahead of Time compilation.", + "x-user-analytics": "ep.ng_aot", + "default": true + }, + "sourceMap": { + "description": "Output source maps for scripts and styles. For more information, see https://angular.dev/reference/configs/workspace-config#source-map-configuration.", + "default": false, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output source maps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output source maps for all styles.", + "default": true + }, + "hidden": { + "type": "boolean", + "description": "Output source maps used for error reporting tools.", + "default": false + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages source maps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + }, + "vendorChunk": { + "type": "boolean", + "description": "Generate a seperate bundle containing only vendor libraries. This option should only be used for development to reduce the incremental compilation time.", + "default": false + }, + "commonChunk": { + "type": "boolean", + "description": "Generate a seperate bundle containing code used across multiple bundles.", + "default": true + }, + "baseHref": { + "type": "string", + "description": "Base url for the application being built." + }, + "deployUrl": { + "type": "string", + "description": "Customize the base path for the URLs of resources in 'index.html' and component stylesheets. This option is only necessary for specific deployment scenarios, such as with Angular Elements or when utilizing different CDN locations." + }, + "verbose": { + "type": "boolean", + "description": "Adds more details to output logging.", + "default": false + }, + "progress": { + "type": "boolean", + "description": "Log progress to the console while building.", + "default": true + }, + "i18nMissingTranslation": { + "type": "string", + "description": "How to handle missing translations for i18n.", + "enum": ["warning", "error", "ignore"], + "default": "warning" + }, + "i18nDuplicateTranslation": { + "type": "string", + "description": "How to handle duplicate translations for i18n.", + "enum": ["warning", "error", "ignore"], + "default": "warning" + }, + "localize": { + "description": "Translate the bundles in one or more locales.", + "oneOf": [ + { + "type": "boolean", + "description": "Translate all locales." + }, + { + "type": "array", + "description": "List of locales ID's to translate.", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-[a-zA-Z]{5,8})?(-x(-[a-zA-Z0-9]{1,8})+)?$" + } + } + ] + }, + "watch": { + "type": "boolean", + "description": "Run build when files change.", + "default": false + }, + "outputHashing": { + "type": "string", + "description": "Define the output filename cache-busting hashing mode.", + "default": "none", + "enum": ["none", "all", "media", "bundles"] + }, + "poll": { + "type": "number", + "description": "Enable and define the file watching poll time period in milliseconds." + }, + "deleteOutputPath": { + "type": "boolean", + "description": "Delete the output path before building.", + "default": true + }, + "preserveSymlinks": { + "type": "boolean", + "description": "Do not use the real path when resolving modules. If unset then will default to `true` if NodeJS option --preserve-symlinks is set." + }, + "extractLicenses": { + "type": "boolean", + "description": "Extract all licenses in a separate file.", + "default": true + }, + "buildOptimizer": { + "type": "boolean", + "description": "Enables advanced build optimizations when using the 'aot' option.", + "default": true + }, + "namedChunks": { + "type": "boolean", + "description": "Use file name for lazy loaded chunks.", + "default": false + }, + "subresourceIntegrity": { + "type": "boolean", + "description": "Enables the use of subresource integrity validation.", + "default": false + }, + "serviceWorker": { + "type": "boolean", + "description": "Generates a service worker config for production builds.", + "default": false + }, + "ngswConfigPath": { + "type": "string", + "description": "Path to ngsw-config.json." + }, + "index": { + "description": "Configures the generation of the application's HTML index.", + "oneOf": [ + { + "type": "string", + "description": "The path of a file to use for the application's HTML index. The filename of the specified path will be used for the generated file and will be created in the root of the application's configured output path." + }, + { + "type": "object", + "description": "", + "properties": { + "input": { + "type": "string", + "minLength": 1, + "description": "The path of a file to use for the application's generated HTML index." + }, + "output": { + "type": "string", + "minLength": 1, + "default": "index.html", + "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." + } + }, + "required": ["input"] + }, + { + "const": false, + "type": "boolean", + "description": "Does not generate an `index.html` file." + } + ] + }, + "statsJson": { + "type": "boolean", + "description": "Generates a 'stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", + "default": false + }, + "budgets": { + "description": "Budget thresholds to ensure parts of your application stay within boundaries which you set.", + "type": "array", + "items": { + "$ref": "#/definitions/budget" + }, + "default": [] + }, + "webWorkerTsConfig": { + "type": "string", + "description": "TypeScript configuration for Web Worker modules." + }, + "crossOrigin": { + "type": "string", + "description": "Define the crossorigin attribute setting of elements that provide CORS support.", + "default": "none", + "enum": ["none", "anonymous", "use-credentials"] + }, + "allowedCommonJsDependencies": { + "description": "A list of CommonJS or AMD packages that are allowed to be used without a build time warning. Use `'*'` to allow all.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + } + }, + "additionalProperties": false, + "required": ["outputPath", "index", "main", "tsConfig"], + "definitions": { + "assetPattern": { + "oneOf": [ + { + "type": "object", + "properties": { + "followSymlinks": { + "type": "boolean", + "default": false, + "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched." + }, + "glob": { + "type": "string", + "description": "The pattern to match." + }, + "input": { + "type": "string", + "description": "The input directory path in which to apply 'glob'. Defaults to the project root." + }, + "ignore": { + "description": "An array of globs to ignore.", + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "string", + "default": "", + "description": "Absolute path within the output." + } + }, + "additionalProperties": false, + "required": ["glob", "input"] + }, + { + "type": "string" + } + ] + }, + "fileReplacement": { + "type": "object", + "properties": { + "replace": { + "type": "string", + "pattern": "\\.(([cm]?[jt])sx?|json)$" + }, + "with": { + "type": "string", + "pattern": "\\.(([cm]?[jt])sx?|json)$" + } + }, + "additionalProperties": false, + "required": ["replace", "with"] + }, + "budget": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of budget.", + "enum": ["all", "allScript", "any", "anyScript", "anyComponentStyle", "bundle", "initial"] + }, + "name": { + "type": "string", + "description": "The name of the bundle." + }, + "baseline": { + "type": "string", + "description": "The baseline size for comparison." + }, + "maximumWarning": { + "type": "string", + "description": "The maximum threshold for warning relative to the baseline." + }, + "maximumError": { + "type": "string", + "description": "The maximum threshold for error relative to the baseline." + }, + "minimumWarning": { + "type": "string", + "description": "The minimum threshold for warning relative to the baseline." + }, + "minimumError": { + "type": "string", + "description": "The minimum threshold for error relative to the baseline." + }, + "warning": { + "type": "string", + "description": "The threshold for warning relative to the baseline (min & max)." + }, + "error": { + "type": "string", + "description": "The threshold for error relative to the baseline (min & max)." + } + }, + "additionalProperties": false, + "required": ["type"] + } + } +} diff --git a/src/builders/browser/index.d.ts b/src/builders/browser/index.d.ts new file mode 100644 index 000000000..5dedc25aa --- /dev/null +++ b/src/builders/browser/index.d.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { IndexHtmlTransform } from '@angular/build/private'; +import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import { WebpackLoggingCallback } from '@angular-devkit/build-webpack'; +import { Observable } from 'rxjs'; +import webpack from 'webpack'; +import { BuildEventStats } from '../../tools/webpack/utils/stats'; +import { ExecutionTransformer } from '../../transforms'; +import { Schema as BrowserBuilderSchema } from './schema'; +/** + * @experimental Direct usage of this type is considered experimental. + */ +export type BrowserBuilderOutput = BuilderOutput & { + stats: BuildEventStats; + baseOutputPath: string; + outputs: { + locale?: string; + path: string; + baseHref?: string; + }[]; +}; +/** + * Maximum time in milliseconds for single build/rebuild + * This accounts for CI variability. + */ +export declare const BUILD_TIMEOUT = 30000; +/** + * @experimental Direct usage of this function is considered experimental. + */ +export declare function buildWebpackBrowser(options: BrowserBuilderSchema, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; + logging?: WebpackLoggingCallback; + indexHtml?: IndexHtmlTransform; +}): Observable; +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; diff --git a/src/builders/browser/index.js b/src/builders/browser/index.js new file mode 100644 index 000000000..cea59eb95 --- /dev/null +++ b/src/builders/browser/index.js @@ -0,0 +1,342 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BUILD_TIMEOUT = void 0; +exports.buildWebpackBrowser = buildWebpackBrowser; +const private_1 = require("@angular/build/private"); +const architect_1 = require("@angular-devkit/architect"); +const build_webpack_1 = require("@angular-devkit/build-webpack"); +const webpack_1 = require("@ngtools/webpack"); +const fs = __importStar(require("node:fs")); +const path = __importStar(require("node:path")); +const rxjs_1 = require("rxjs"); +const configs_1 = require("../../tools/webpack/configs"); +const async_chunks_1 = require("../../tools/webpack/utils/async-chunks"); +const helpers_1 = require("../../tools/webpack/utils/helpers"); +const stats_1 = require("../../tools/webpack/utils/stats"); +const utils_1 = require("../../utils"); +const color_1 = require("../../utils/color"); +const copy_assets_1 = require("../../utils/copy-assets"); +const error_1 = require("../../utils/error"); +const i18n_inlining_1 = require("../../utils/i18n-inlining"); +const normalize_cache_1 = require("../../utils/normalize-cache"); +const output_paths_1 = require("../../utils/output-paths"); +const package_chunk_sort_1 = require("../../utils/package-chunk-sort"); +const spinner_1 = require("../../utils/spinner"); +const webpack_browser_config_1 = require("../../utils/webpack-browser-config"); +/** + * Maximum time in milliseconds for single build/rebuild + * This accounts for CI variability. + */ +exports.BUILD_TIMEOUT = 30_000; +async function initialize(options, context, webpackConfigurationTransform) { + const originalOutputPath = options.outputPath; + // Assets are processed directly by the builder except when watching + const adjustedOptions = options.watch ? options : { ...options, assets: [] }; + const { config, projectRoot, projectSourceRoot, i18n } = await (0, webpack_browser_config_1.generateI18nBrowserWebpackConfigFromContext)(adjustedOptions, context, (wco) => [ + (0, configs_1.getCommonConfig)(wco), + (0, configs_1.getStylesConfig)(wco), + ]); + let transformedConfig; + if (webpackConfigurationTransform) { + transformedConfig = await webpackConfigurationTransform(config); + } + if (options.deleteOutputPath) { + await (0, utils_1.deleteOutputDir)(context.workspaceRoot, originalOutputPath); + } + return { config: transformedConfig || config, projectRoot, projectSourceRoot, i18n }; +} +/** + * @experimental Direct usage of this function is considered experimental. + */ +// eslint-disable-next-line max-lines-per-function +function buildWebpackBrowser(options, context, transforms = {}) { + const projectName = context.target?.project; + if (!projectName) { + throw new Error('The builder requires a target.'); + } + const baseOutputPath = path.resolve(context.workspaceRoot, options.outputPath); + let outputPaths; + // Check Angular version. + (0, private_1.assertCompatibleAngularVersion)(context.workspaceRoot); + return (0, rxjs_1.from)(context.getProjectMetadata(projectName)).pipe((0, rxjs_1.switchMap)(async (projectMetadata) => { + // Purge old build disk cache. + await (0, private_1.purgeStaleBuildCache)(context); + // Initialize builder + const initialization = await initialize(options, context, transforms.webpackConfiguration); + // Add index file to watched files. + if (options.watch) { + const indexInputFile = path.join(context.workspaceRoot, (0, webpack_browser_config_1.getIndexInputFile)(options.index)); + initialization.config.plugins ??= []; + initialization.config.plugins.push({ + apply: (compiler) => { + compiler.hooks.thisCompilation.tap('build-angular', (compilation) => { + compilation.fileDependencies.add(indexInputFile); + }); + }, + }); + } + return { + ...initialization, + cacheOptions: (0, normalize_cache_1.normalizeCacheOptions)(projectMetadata, context.workspaceRoot), + }; + }), (0, rxjs_1.switchMap)( + // eslint-disable-next-line max-lines-per-function + ({ config, projectRoot, projectSourceRoot, i18n, cacheOptions }) => { + const normalizedOptimization = (0, utils_1.normalizeOptimization)(options.optimization); + return (0, build_webpack_1.runWebpack)(config, context, { + webpackFactory: require('webpack'), + logging: transforms.logging || + ((stats, config) => { + if (options.verbose && config.stats !== false) { + const statsOptions = config.stats === true ? undefined : config.stats; + context.logger.info(stats.toString(statsOptions)); + } + }), + }).pipe((0, rxjs_1.concatMap)( + // eslint-disable-next-line max-lines-per-function + async (buildEvent) => { + const spinner = new spinner_1.Spinner(); + spinner.enabled = options.progress !== false; + const { success, emittedFiles = [], outputPath: webpackOutputPath } = buildEvent; + const webpackRawStats = buildEvent.webpackStats; + if (!webpackRawStats) { + throw new Error('Webpack stats build result is required.'); + } + // Fix incorrectly set `initial` value on chunks. + const extraEntryPoints = [ + ...(0, helpers_1.normalizeExtraEntryPoints)(options.styles || [], 'styles'), + ...(0, helpers_1.normalizeExtraEntryPoints)(options.scripts || [], 'scripts'), + ]; + const webpackStats = { + ...webpackRawStats, + chunks: (0, async_chunks_1.markAsyncChunksNonInitial)(webpackRawStats, extraEntryPoints), + }; + if (!success) { + // If using bundle downleveling then there is only one build + // If it fails show any diagnostic messages and bail + if ((0, stats_1.statsHasWarnings)(webpackStats)) { + context.logger.warn((0, stats_1.statsWarningsToString)(webpackStats, { colors: true })); + } + if ((0, stats_1.statsHasErrors)(webpackStats)) { + context.logger.error((0, stats_1.statsErrorsToString)(webpackStats, { colors: true })); + } + return { + webpackStats: webpackRawStats, + output: { success: false }, + }; + } + else { + outputPaths = (0, output_paths_1.ensureOutputPaths)(baseOutputPath, i18n); + const scriptsEntryPointName = (0, helpers_1.normalizeExtraEntryPoints)(options.scripts || [], 'scripts').map((x) => x.bundleName); + if (i18n.shouldInline) { + const success = await (0, i18n_inlining_1.i18nInlineEmittedFiles)(context, emittedFiles, i18n, baseOutputPath, Array.from(outputPaths.values()), scriptsEntryPointName, webpackOutputPath, options.i18nMissingTranslation); + if (!success) { + return { + webpackStats: webpackRawStats, + output: { success: false }, + }; + } + } + // Check for budget errors and display them to the user. + const budgets = options.budgets; + let budgetFailures; + if (budgets?.length) { + budgetFailures = [...(0, private_1.checkBudgets)(budgets, webpackStats)]; + for (const { severity, message } of budgetFailures) { + switch (severity) { + case private_1.ThresholdSeverity.Warning: + webpackStats.warnings?.push({ message }); + break; + case private_1.ThresholdSeverity.Error: + webpackStats.errors?.push({ message }); + break; + default: + assertNever(severity); + } + } + } + const buildSuccess = success && !(0, stats_1.statsHasErrors)(webpackStats); + if (buildSuccess) { + // Copy assets + if (!options.watch && options.assets?.length) { + spinner.start('Copying assets...'); + try { + await (0, copy_assets_1.copyAssets)((0, utils_1.normalizeAssetPatterns)(options.assets, context.workspaceRoot, projectRoot, projectSourceRoot), Array.from(outputPaths.values()), context.workspaceRoot); + spinner.succeed('Copying assets complete.'); + } + catch (err) { + spinner.fail(color_1.colors.redBright('Copying of assets failed.')); + (0, error_1.assertIsError)(err); + return { + output: { + success: false, + error: 'Unable to copy assets: ' + err.message, + }, + webpackStats: webpackRawStats, + }; + } + } + if (options.index) { + spinner.start('Generating index html...'); + const entrypoints = (0, package_chunk_sort_1.generateEntryPoints)({ + scripts: options.scripts ?? [], + styles: options.styles ?? [], + }); + const indexHtmlGenerator = new private_1.IndexHtmlGenerator({ + cache: cacheOptions, + indexPath: path.join(context.workspaceRoot, (0, webpack_browser_config_1.getIndexInputFile)(options.index)), + entrypoints, + deployUrl: options.deployUrl, + sri: options.subresourceIntegrity, + optimization: normalizedOptimization, + crossOrigin: options.crossOrigin, + postTransform: transforms.indexHtml, + imageDomains: Array.from(webpack_1.imageDomains), + }); + let hasErrors = false; + for (const [locale, outputPath] of outputPaths.entries()) { + try { + const { csrContent: content, warnings, errors, } = await indexHtmlGenerator.process({ + baseHref: getLocaleBaseHref(i18n, locale) ?? options.baseHref, + // i18nLocale is used when Ivy is disabled + lang: locale || undefined, + outputPath, + files: mapEmittedFilesToFileInfo(emittedFiles), + }); + if (warnings.length || errors.length) { + spinner.stop(); + warnings.forEach((m) => context.logger.warn(m)); + errors.forEach((m) => { + context.logger.error(m); + hasErrors = true; + }); + spinner.start(); + } + const indexOutput = path.join(outputPath, (0, webpack_browser_config_1.getIndexOutputFile)(options.index)); + await fs.promises.mkdir(path.dirname(indexOutput), { recursive: true }); + await fs.promises.writeFile(indexOutput, content); + } + catch (error) { + spinner.fail('Index html generation failed.'); + (0, error_1.assertIsError)(error); + return { + webpackStats: webpackRawStats, + output: { success: false, error: error.message }, + }; + } + } + if (hasErrors) { + spinner.fail('Index html generation failed.'); + return { + webpackStats: webpackRawStats, + output: { success: false }, + }; + } + else { + spinner.succeed('Index html generation complete.'); + } + } + if (options.serviceWorker) { + spinner.start('Generating service worker...'); + for (const [locale, outputPath] of outputPaths.entries()) { + try { + await (0, private_1.augmentAppWithServiceWorker)(projectRoot, context.workspaceRoot, outputPath, getLocaleBaseHref(i18n, locale) ?? options.baseHref ?? '/', options.ngswConfigPath); + } + catch (error) { + spinner.fail('Service worker generation failed.'); + (0, error_1.assertIsError)(error); + return { + webpackStats: webpackRawStats, + output: { success: false, error: error.message }, + }; + } + } + spinner.succeed('Service worker generation complete.'); + } + } + (0, stats_1.webpackStatsLogger)(context.logger, webpackStats, config, budgetFailures); + return { + webpackStats: webpackRawStats, + output: { success: buildSuccess }, + }; + } + }), (0, rxjs_1.map)(({ output: event, webpackStats }) => ({ + ...event, + stats: (0, stats_1.generateBuildEventStats)(webpackStats, options), + baseOutputPath, + outputs: (outputPaths && + [...outputPaths.entries()].map(([locale, path]) => ({ + locale, + path, + baseHref: getLocaleBaseHref(i18n, locale) ?? options.baseHref, + }))) || { + path: baseOutputPath, + baseHref: options.baseHref, + }, + }))); + })); + function getLocaleBaseHref(i18n, locale) { + if (i18n.flatOutput) { + return undefined; + } + const localeData = i18n.locales[locale]; + if (!localeData) { + return undefined; + } + const baseHrefSuffix = localeData.baseHref ?? localeData.subPath + '/'; + return baseHrefSuffix !== '' ? (0, utils_1.urlJoin)(options.baseHref || '', baseHrefSuffix) : undefined; + } +} +function assertNever(input) { + throw new Error(`Unexpected call to assertNever() with input: ${JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`); +} +function mapEmittedFilesToFileInfo(files = []) { + const filteredFiles = []; + for (const { file, name, extension, initial } of files) { + if (name && initial) { + filteredFiles.push({ file, extension, name }); + } + } + return filteredFiles; +} +exports.default = (0, architect_1.createBuilder)(buildWebpackBrowser); diff --git a/src/browser/schema.d.ts b/src/builders/browser/schema.d.ts similarity index 56% rename from src/browser/schema.d.ts rename to src/builders/browser/schema.d.ts index 3d3f325e6..e7005cee6 100644 --- a/src/browser/schema.d.ts +++ b/src/builders/browser/schema.d.ts @@ -1,9 +1,10 @@ /** * Browser target options */ -export interface Schema { +export type Schema = { /** - * A list of CommonJS packages that are allowed to be used without a build time warning. + * A list of CommonJS or AMD packages that are allowed to be used without a build time + * warning. Use `'*'` to allow all. */ allowedCommonJsDependencies?: string[]; /** @@ -24,11 +25,11 @@ export interface Schema { */ budgets?: Budget[]; /** - * Enables '@angular-devkit/build-optimizer' optimizations when using the 'aot' option. + * Enables advanced build optimizations when using the 'aot' option. */ buildOptimizer?: boolean; /** - * Use a separate bundle containing code used across multiple bundles. + * Generate a seperate bundle containing code used across multiple bundles. */ commonChunk?: boolean; /** @@ -40,59 +41,38 @@ export interface Schema { */ deleteOutputPath?: boolean; /** - * URL where files will be deployed. + * Customize the base path for the URLs of resources in 'index.html' and component + * stylesheets. This option is only necessary for specific deployment scenarios, such as + * with Angular Elements or when utilizing different CDN locations. */ deployUrl?: string; - /** - * Concatenate modules with Rollup before bundling them with Webpack. - */ - experimentalRollupPass?: boolean; - /** - * Extract css from global styles into css files instead of js ones. - */ - extractCss?: boolean; /** * Extract all licenses in a separate file. */ extractLicenses?: boolean; /** - * Replace files with other files in the build. + * Replace compilation source files with other compilation source files in the build. */ fileReplacements?: FileReplacement[]; /** - * Run the TypeScript type checker in a forked process. - */ - forkTypeChecker?: boolean; - /** - * Localization file to use for i18n. - * @deprecated Use 'locales' object in the project metadata instead. - */ - i18nFile?: string; - /** - * Format of the localization file specified with --i18n-file. - * @deprecated No longer needed as the format will be determined automatically. + * How to handle duplicate translations for i18n. */ - i18nFormat?: string; - /** - * Locale to use for i18n. - * @deprecated Use 'localize' instead. - */ - i18nLocale?: string; + i18nDuplicateTranslation?: I18NTranslation; /** * How to handle missing translations for i18n. */ - i18nMissingTranslation?: I18NMissingTranslation; + i18nMissingTranslation?: I18NTranslation; /** * Configures the generation of the application's HTML index. */ index: IndexUnion; /** - * List of additional NgModule files that will be lazy loaded. Lazy router modules will be - * discovered automatically. - * @deprecated 'SystemJsNgModuleLoader' is deprecated, and this is part of its usage. Use - * 'import()' syntax instead. + * The stylesheet language to use for the application's inline component styles. + */ + inlineStyleLanguage?: InlineStyleLanguage; + /** + * Translate the bundles in one or more locales. */ - lazyModules?: string[]; localize?: Localize; /** * The full path for the main entry point to the app, relative to the current workspace. @@ -107,7 +87,10 @@ export interface Schema { */ ngswConfigPath?: string; /** - * Enables optimization of the build output. + * Enables optimization of the build output. Including minification of scripts and styles, + * tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For + * more information, see + * https://angular.dev/reference/configs/workspace-config#optimization-configuration. */ optimization?: OptimizationUnion; /** @@ -116,8 +99,6 @@ export interface Schema { outputHashing?: OutputHashing; /** * The full path for the new output directory, relative to the current workspace. - * - * By default, writes output to a folder named dist/ in the current project. */ outputPath: string; /** @@ -125,9 +106,9 @@ export interface Schema { */ poll?: number; /** - * The full path for the polyfills file, relative to the current workspace. + * Polyfills to be included in the build. */ - polyfills?: string; + polyfills?: Polyfills; /** * Do not use the real path when resolving modules. If unset then will default to `true` if * NodeJS option --preserve-symlinks is set. @@ -137,13 +118,6 @@ export interface Schema { * Log progress to the console while building. */ progress?: boolean; - /** - * Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only - * for compatibility and transition. The behavior of this option is non-standard and will be - * removed in the next major release. - * @deprecated - */ - rebaseRootRelativeCssUrls?: boolean; /** * The path where style resources will be placed, relative to outputPath. */ @@ -151,17 +125,14 @@ export interface Schema { /** * Global scripts to be included in the build. */ - scripts?: ExtraEntryPoint[]; + scripts?: ScriptElement[]; /** * Generates a service worker config for production builds. */ serviceWorker?: boolean; /** - * Show circular dependency warnings on builds. - */ - showCircularDependencies?: boolean; - /** - * Output sourcemaps. + * Output source maps for scripts and styles. For more information, see + * https://angular.dev/reference/configs/workspace-config#source-map-configuration. */ sourceMap?: SourceMapUnion; /** @@ -176,7 +147,7 @@ export interface Schema { /** * Global styles to be included in the build. */ - styles?: ExtraEntryPoint[]; + styles?: StyleElement[]; /** * Enables the use of subresource integrity validation. */ @@ -186,7 +157,8 @@ export interface Schema { */ tsConfig: string; /** - * Use a separate bundle containing only vendor libraries. + * Generate a seperate bundle containing only vendor libraries. This option should only be + * used for development to reduce the incremental compilation time. */ vendorChunk?: boolean; /** @@ -201,9 +173,14 @@ export interface Schema { * TypeScript configuration for Web Worker modules. */ webWorkerTsConfig?: string; -} -export declare type AssetPattern = AssetPatternClass | string; -export interface AssetPatternClass { +}; +export type AssetPattern = AssetPatternClass | string; +export type AssetPatternClass = { + /** + * Allow glob patterns to follow symlink directories. This allows subdirectories of the + * symlink to be searched. + */ + followSymlinks?: boolean; /** * The pattern to match. */ @@ -219,9 +196,9 @@ export interface AssetPatternClass { /** * Absolute path within the output. */ - output: string; -} -export interface Budget { + output?: string; +}; +export type Budget = { /** * The baseline size for comparison. */ @@ -258,7 +235,7 @@ export interface Budget { * The threshold for warning relative to the baseline (min & max). */ warning?: string; -} +}; /** * The type of budget. */ @@ -279,16 +256,18 @@ export declare enum CrossOrigin { None = "none", UseCredentials = "use-credentials" } -export interface FileReplacement { +export type FileReplacement = { replace?: string; replaceWith?: string; src?: string; with?: string; -} +}; /** + * How to handle duplicate translations for i18n. + * * How to handle missing translations for i18n. */ -export declare enum I18NMissingTranslation { +export declare enum I18NTranslation { Error = "error", Ignore = "ignore", Warning = "warning" @@ -296,8 +275,8 @@ export declare enum I18NMissingTranslation { /** * Configures the generation of the application's HTML index. */ -export declare type IndexUnion = IndexObject | string; -export interface IndexObject { +export type IndexUnion = IndexObject | string; +export type IndexObject = { /** * The path of a file to use for the application's generated HTML index. */ @@ -307,13 +286,34 @@ export interface IndexObject { * will be used and will be considered relative to the application's configured output path. */ output?: string; + [property: string]: any; +}; +/** + * The stylesheet language to use for the application's inline component styles. + */ +export declare enum InlineStyleLanguage { + Css = "css", + Less = "less", + Sass = "sass", + Scss = "scss" } -export declare type Localize = string[] | boolean; /** - * Enables optimization of the build output. + * Translate the bundles in one or more locales. + */ +export type Localize = string[] | boolean; +/** + * Enables optimization of the build output. Including minification of scripts and styles, + * tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For + * more information, see + * https://angular.dev/reference/configs/workspace-config#optimization-configuration. */ -export declare type OptimizationUnion = boolean | OptimizationClass; -export interface OptimizationClass { +export type OptimizationUnion = boolean | OptimizationClass; +export type OptimizationClass = { + /** + * Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` + * environment variable can be used to specify a proxy server. + */ + fonts?: FontsUnion; /** * Enables optimization of the scripts output. */ @@ -321,8 +321,36 @@ export interface OptimizationClass { /** * Enables optimization of the styles output. */ - styles?: boolean; -} + styles?: StylesUnion; +}; +/** + * Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` + * environment variable can be used to specify a proxy server. + */ +export type FontsUnion = boolean | FontsClass; +export type FontsClass = { + /** + * Reduce render blocking requests by inlining external Google Fonts and Adobe Fonts CSS + * definitions in the application's HTML index file. This option requires internet access. + * `HTTPS_PROXY` environment variable can be used to specify a proxy server. + */ + inline?: boolean; +}; +/** + * Enables optimization of the styles output. + */ +export type StylesUnion = boolean | StylesClass; +export type StylesClass = { + /** + * Extract and inline critical CSS definitions to improve first paint time. + */ + inlineCritical?: boolean; + /** + * Minify CSS definitions by removing extraneous whitespace and comments, merging + * identifiers and minimizing values. + */ + minify?: boolean; +}; /** * Define the output filename cache-busting hashing mode. */ @@ -332,8 +360,12 @@ export declare enum OutputHashing { Media = "media", None = "none" } -export declare type ExtraEntryPoint = ExtraEntryPointClass | string; -export interface ExtraEntryPointClass { +/** + * Polyfills to be included in the build. + */ +export type Polyfills = string[] | string; +export type ScriptElement = ScriptClass | string; +export type ScriptClass = { /** * The bundle name for this extra entry point. */ @@ -346,39 +378,51 @@ export interface ExtraEntryPointClass { * The file to include. */ input: string; - /** - * If the bundle will be lazy loaded. - */ - lazy?: boolean; -} +}; /** - * Output sourcemaps. + * Output source maps for scripts and styles. For more information, see + * https://angular.dev/reference/configs/workspace-config#source-map-configuration. */ -export declare type SourceMapUnion = boolean | SourceMapClass; -export interface SourceMapClass { +export type SourceMapUnion = boolean | SourceMapClass; +export type SourceMapClass = { /** - * Output sourcemaps used for error reporting tools. + * Output source maps used for error reporting tools. */ hidden?: boolean; /** - * Output sourcemaps for all scripts. + * Output source maps for all scripts. */ scripts?: boolean; /** - * Output sourcemaps for all styles. + * Output source maps for all styles. */ styles?: boolean; /** - * Resolve vendor packages sourcemaps. + * Resolve vendor packages source maps. */ vendor?: boolean; -} +}; /** * Options to pass to style preprocessors. */ -export interface StylePreprocessorOptions { +export type StylePreprocessorOptions = { /** - * Paths to include. Paths will be resolved to project root. + * Paths to include. Paths will be resolved to workspace root. */ includePaths?: string[]; -} +}; +export type StyleElement = StyleClass | string; +export type StyleClass = { + /** + * The bundle name for this extra entry point. + */ + bundleName?: string; + /** + * If the bundle will be referenced in the HTML file. + */ + inject?: boolean; + /** + * The file to include. + */ + input: string; +}; diff --git a/src/builders/browser/schema.js b/src/builders/browser/schema.js new file mode 100644 index 000000000..b721d8278 --- /dev/null +++ b/src/builders/browser/schema.js @@ -0,0 +1,58 @@ +"use strict"; +// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE +// CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OutputHashing = exports.InlineStyleLanguage = exports.I18NTranslation = exports.CrossOrigin = exports.Type = void 0; +/** + * The type of budget. + */ +var Type; +(function (Type) { + Type["All"] = "all"; + Type["AllScript"] = "allScript"; + Type["Any"] = "any"; + Type["AnyComponentStyle"] = "anyComponentStyle"; + Type["AnyScript"] = "anyScript"; + Type["Bundle"] = "bundle"; + Type["Initial"] = "initial"; +})(Type || (exports.Type = Type = {})); +/** + * Define the crossorigin attribute setting of elements that provide CORS support. + */ +var CrossOrigin; +(function (CrossOrigin) { + CrossOrigin["Anonymous"] = "anonymous"; + CrossOrigin["None"] = "none"; + CrossOrigin["UseCredentials"] = "use-credentials"; +})(CrossOrigin || (exports.CrossOrigin = CrossOrigin = {})); +/** + * How to handle duplicate translations for i18n. + * + * How to handle missing translations for i18n. + */ +var I18NTranslation; +(function (I18NTranslation) { + I18NTranslation["Error"] = "error"; + I18NTranslation["Ignore"] = "ignore"; + I18NTranslation["Warning"] = "warning"; +})(I18NTranslation || (exports.I18NTranslation = I18NTranslation = {})); +/** + * The stylesheet language to use for the application's inline component styles. + */ +var InlineStyleLanguage; +(function (InlineStyleLanguage) { + InlineStyleLanguage["Css"] = "css"; + InlineStyleLanguage["Less"] = "less"; + InlineStyleLanguage["Sass"] = "sass"; + InlineStyleLanguage["Scss"] = "scss"; +})(InlineStyleLanguage || (exports.InlineStyleLanguage = InlineStyleLanguage = {})); +/** + * Define the output filename cache-busting hashing mode. + */ +var OutputHashing; +(function (OutputHashing) { + OutputHashing["All"] = "all"; + OutputHashing["Bundles"] = "bundles"; + OutputHashing["Media"] = "media"; + OutputHashing["None"] = "none"; +})(OutputHashing || (exports.OutputHashing = OutputHashing = {})); diff --git a/src/browser/schema.json b/src/builders/browser/schema.json similarity index 59% rename from src/browser/schema.json rename to src/builders/browser/schema.json index 9cf9c5f37..f8170c396 100644 --- a/src/browser/schema.json +++ b/src/builders/browser/schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/schema", + "$schema": "http://json-schema.org/draft-07/schema", "title": "Webpack browser schema for Build Facade.", "description": "Browser target options", "type": "object", @@ -14,12 +14,25 @@ }, "main": { "type": "string", - "description": "The full path for the main entry point to the app, relative to the current workspace.", - "$valueDescription": "fileName" + "description": "The full path for the main entry point to the app, relative to the current workspace." }, "polyfills": { - "type": "string", - "description": "The full path for the polyfills file, relative to the current workspace." + "description": "Polyfills to be included in the build.", + "oneOf": [ + { + "type": "array", + "description": "A list of polyfills to include in the build. Can be a full path for a file, relative to the current workspace or module specifier. Example: 'zone.js'.", + "items": { + "type": "string", + "uniqueItems": true + }, + "default": [] + }, + { + "type": "string", + "description": "The full path for the polyfills file, relative to the current workspace or a module specifier. Example: 'zone.js'." + } + ] }, "tsConfig": { "type": "string", @@ -30,7 +43,35 @@ "type": "array", "default": [], "items": { - "$ref": "#/definitions/extraEntryPoint" + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include.", + "pattern": "\\.[cm]?jsx?$" + }, + "bundleName": { + "type": "string", + "pattern": "^[\\w\\-.]*$", + "description": "The bundle name for this extra entry point." + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true + } + }, + "additionalProperties": false, + "required": ["input"] + }, + { + "type": "string", + "description": "The file to include.", + "pattern": "\\.[cm]?jsx?$" + } + ] } }, "styles": { @@ -38,15 +79,49 @@ "type": "array", "default": [], "items": { - "$ref": "#/definitions/extraEntryPoint" + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include.", + "pattern": "\\.(?:css|scss|sass|less)$" + }, + "bundleName": { + "type": "string", + "pattern": "^[\\w\\-.]*$", + "description": "The bundle name for this extra entry point." + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true + } + }, + "additionalProperties": false, + "required": ["input"] + }, + { + "type": "string", + "description": "The file to include.", + "pattern": "\\.(?:css|scss|sass|less)$" + } + ] } }, + "inlineStyleLanguage": { + "description": "The stylesheet language to use for the application's inline component styles.", + "type": "string", + "default": "css", + "enum": ["css", "less", "sass", "scss"] + }, "stylePreprocessorOptions": { "description": "Options to pass to style preprocessors.", "type": "object", "properties": { "includePaths": { - "description": "Paths to include. Paths will be resolved to project root.", + "description": "Paths to include. Paths will be resolved to workspace root.", "type": "array", "items": { "type": "string" @@ -57,8 +132,9 @@ "additionalProperties": false }, "optimization": { - "description": "Enables optimization of the build output.", - "x-user-analytics": 16, + "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For more information, see https://angular.dev/reference/configs/workspace-config#optimization-configuration.", + "default": true, + "x-user-analytics": "ep.ng_optimization", "oneOf": [ { "type": "object", @@ -69,9 +145,49 @@ "default": true }, "styles": { - "type": "boolean", "description": "Enables optimization of the styles output.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "minify": { + "type": "boolean", + "description": "Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers and minimizing values.", + "default": true + }, + "inlineCritical": { + "type": "boolean", + "description": "Extract and inline critical CSS definitions to improve first paint time.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + }, + "fonts": { + "description": "Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "inline": { + "type": "boolean", + "description": "Reduce render blocking requests by inlining external Google Fonts and Adobe Fonts CSS definitions in the application's HTML index file. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] } }, "additionalProperties": false @@ -82,7 +198,7 @@ ] }, "fileReplacements": { - "description": "Replace files with other files in the build.", + "description": "Replace compilation source files with other compilation source files in the build.", "type": "array", "items": { "$ref": "#/definitions/fileReplacement" @@ -91,44 +207,43 @@ }, "outputPath": { "type": "string", - "description": "The full path for the new output directory, relative to the current workspace.\n\nBy default, writes output to a folder named dist/ in the current project." + "description": "The full path for the new output directory, relative to the current workspace." }, "resourcesOutputPath": { "type": "string", - "description": "The path where style resources will be placed, relative to outputPath.", - "default": "" + "description": "The path where style resources will be placed, relative to outputPath." }, "aot": { "type": "boolean", "description": "Build using Ahead of Time compilation.", - "x-user-analytics": 13, - "default": false + "x-user-analytics": "ep.ng_aot", + "default": true }, "sourceMap": { - "description": "Output sourcemaps.", - "default": true, + "description": "Output source maps for scripts and styles. For more information, see https://angular.dev/reference/configs/workspace-config#source-map-configuration.", + "default": false, "oneOf": [ { "type": "object", "properties": { "scripts": { "type": "boolean", - "description": "Output sourcemaps for all scripts.", + "description": "Output source maps for all scripts.", "default": true }, "styles": { "type": "boolean", - "description": "Output sourcemaps for all styles.", + "description": "Output source maps for all styles.", "default": true }, "hidden": { "type": "boolean", - "description": "Output sourcemaps used for error reporting tools.", + "description": "Output source maps used for error reporting tools.", "default": false }, "vendor": { "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", + "description": "Resolve vendor packages source maps.", "default": false } }, @@ -141,12 +256,12 @@ }, "vendorChunk": { "type": "boolean", - "description": "Use a separate bundle containing only vendor libraries.", - "default": true + "description": "Generate a seperate bundle containing only vendor libraries. This option should only be used for development to reduce the incremental compilation time.", + "default": false }, "commonChunk": { "type": "boolean", - "description": "Use a separate bundle containing code used across multiple bundles.", + "description": "Generate a seperate bundle containing code used across multiple bundles.", "default": true }, "baseHref": { @@ -155,7 +270,7 @@ }, "deployUrl": { "type": "string", - "description": "URL where files will be deployed." + "description": "Customize the base path for the URLs of resources in 'index.html' and component stylesheets. This option is only necessary for specific deployment scenarios, such as with Angular Elements or when utilizing different CDN locations." }, "verbose": { "type": "boolean", @@ -164,22 +279,8 @@ }, "progress": { "type": "boolean", - "description": "Log progress to the console while building." - }, - "i18nFile": { - "type": "string", - "description": "Localization file to use for i18n.", - "x-deprecated": "Use 'locales' object in the project metadata instead." - }, - "i18nFormat": { - "type": "string", - "description": "Format of the localization file specified with --i18n-file.", - "x-deprecated": "No longer needed as the format will be determined automatically." - }, - "i18nLocale": { - "type": "string", - "description": "Locale to use for i18n.", - "x-deprecated": "Use 'localize' instead." + "description": "Log progress to the console while building.", + "default": true }, "i18nMissingTranslation": { "type": "string", @@ -187,7 +288,14 @@ "enum": ["warning", "error", "ignore"], "default": "warning" }, + "i18nDuplicateTranslation": { + "type": "string", + "description": "How to handle duplicate translations for i18n.", + "enum": ["warning", "error", "ignore"], + "default": "warning" + }, "localize": { + "description": "Translate the bundles in one or more locales.", "oneOf": [ { "type": "boolean", @@ -204,11 +312,6 @@ } ] }, - "extractCss": { - "type": "boolean", - "description": "Extract css from global styles into css files instead of js ones.", - "default": false - }, "watch": { "type": "boolean", "description": "Run build when files change.", @@ -218,12 +321,7 @@ "type": "string", "description": "Define the output filename cache-busting hashing mode.", "default": "none", - "enum": [ - "none", - "all", - "media", - "bundles" - ] + "enum": ["none", "all", "media", "bundles"] }, "poll": { "type": "number", @@ -241,22 +339,17 @@ "extractLicenses": { "type": "boolean", "description": "Extract all licenses in a separate file.", - "default": false - }, - "showCircularDependencies": { - "type": "boolean", - "description": "Show circular dependency warnings on builds.", "default": true }, "buildOptimizer": { "type": "boolean", - "description": "Enables '@angular-devkit/build-optimizer' optimizations when using the 'aot' option.", - "default": false + "description": "Enables advanced build optimizations when using the 'aot' option.", + "default": true }, "namedChunks": { "type": "boolean", "description": "Use file name for lazy loaded chunks.", - "default": true + "default": false }, "subresourceIntegrity": { "type": "boolean", @@ -304,20 +397,6 @@ "description": "Generates a 'stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", "default": false }, - "forkTypeChecker": { - "type": "boolean", - "description": "Run the TypeScript type checker in a forked process.", - "default": true - }, - "lazyModules": { - "description": "List of additional NgModule files that will be lazy loaded. Lazy router modules will be discovered automatically.", - "type": "array", - "items": { - "type": "string" - }, - "x-deprecated": "'SystemJsNgModuleLoader' is deprecated, and this is part of its usage. Use 'import()' syntax instead.", - "default": [] - }, "budgets": { "description": "Budget thresholds to ensure parts of your application stay within boundaries which you set.", "type": "array", @@ -326,12 +405,6 @@ }, "default": [] }, - "rebaseRootRelativeCssUrls": { - "description": "Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only for compatibility and transition. The behavior of this option is non-standard and will be removed in the next major release.", - "type": "boolean", - "default": false, - "x-deprecated": true - }, "webWorkerTsConfig": { "type": "string", "description": "TypeScript configuration for Web Worker modules." @@ -340,19 +413,10 @@ "type": "string", "description": "Define the crossorigin attribute setting of elements that provide CORS support.", "default": "none", - "enum": [ - "none", - "anonymous", - "use-credentials" - ] - }, - "experimentalRollupPass": { - "type": "boolean", - "description": "Concatenate modules with Rollup before bundling them with Webpack.", - "default": false + "enum": ["none", "anonymous", "use-credentials"] }, "allowedCommonJsDependencies": { - "description": "A list of CommonJS packages that are allowed to be used without a build time warning.", + "description": "A list of CommonJS or AMD packages that are allowed to be used without a build time warning. Use `'*'` to allow all.", "type": "array", "items": { "type": "string" @@ -361,18 +425,18 @@ } }, "additionalProperties": false, - "required": [ - "outputPath", - "index", - "main", - "tsConfig" - ], + "required": ["outputPath", "index", "main", "tsConfig"], "definitions": { "assetPattern": { "oneOf": [ { "type": "object", "properties": { + "followSymlinks": { + "type": "boolean", + "default": false, + "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched." + }, "glob": { "type": "string", "description": "The pattern to match." @@ -390,15 +454,12 @@ }, "output": { "type": "string", + "default": "", "description": "Absolute path within the output." } }, "additionalProperties": false, - "required": [ - "glob", - "input", - "output" - ] + "required": ["glob", "input"] }, { "type": "string" @@ -411,69 +472,31 @@ "type": "object", "properties": { "src": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?[jt])sx?|json)$" }, "replaceWith": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?[jt])sx?|json)$" } }, "additionalProperties": false, - "required": [ - "src", - "replaceWith" - ] + "required": ["src", "replaceWith"] }, { "type": "object", "properties": { "replace": { - "type": "string" - }, - "with": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "replace", - "with" - ] - } - ] - }, - "extraEntryPoint": { - "oneOf": [ - { - "type": "object", - "properties": { - "input": { "type": "string", - "description": "The file to include." + "pattern": "\\.(([cm]?[jt])sx?|json)$" }, - "bundleName": { + "with": { "type": "string", - "description": "The bundle name for this extra entry point." - }, - "lazy": { - "type": "boolean", - "description": "If the bundle will be lazy loaded.", - "default": false, - "x-deprecated": "Use 'inject' option with 'false' value instead." - }, - "inject": { - "type": "boolean", - "description": "If the bundle will be referenced in the HTML file.", - "default": true + "pattern": "\\.(([cm]?[jt])sx?|json)$" } }, "additionalProperties": false, - "required": [ - "input" - ] - }, - { - "type": "string", - "description": "The file to include." + "required": ["replace", "with"] } ] }, @@ -483,15 +506,7 @@ "type": { "type": "string", "description": "The type of budget.", - "enum": [ - "all", - "allScript", - "any", - "anyScript", - "anyComponentStyle", - "bundle", - "initial" - ] + "enum": ["all", "allScript", "any", "anyScript", "anyComponentStyle", "bundle", "initial"] }, "name": { "type": "string", @@ -527,9 +542,7 @@ } }, "additionalProperties": false, - "required": [ - "type" - ] + "required": ["type"] } } } diff --git a/src/builders/dev-server/builder.d.ts b/src/builders/dev-server/builder.d.ts new file mode 100644 index 000000000..86c112e83 --- /dev/null +++ b/src/builders/dev-server/builder.d.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { DevServerBuilderOutput } from '@angular/build'; +import { type IndexHtmlTransform } from '@angular/build/private'; +import type { BuilderContext } from '@angular-devkit/architect'; +import type { Plugin } from 'esbuild'; +import type http from 'node:http'; +import { Observable } from 'rxjs'; +import type { ExecutionTransformer } from '../../transforms'; +import type { Schema as DevServerBuilderOptions } from './schema'; +/** + * A Builder that executes a development server based on the provided browser target option. + * + * Usage of the `transforms` and/or `extensions` parameters is NOT supported and may cause + * unexpected build output or build failures. + * + * @param options Dev Server options. + * @param context The build context. + * @param transforms A map of transforms that can be used to hook into some logic (such as + * transforming webpack configuration before passing it to webpack). + * @param extensions An optional object containing an array of build plugins (esbuild-based) + * and/or HTTP request middleware. + * + * @experimental Direct usage of this function is considered experimental. + */ +export declare function execute(options: DevServerBuilderOptions, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; + logging?: import('@angular-devkit/build-webpack').WebpackLoggingCallback; + indexHtml?: IndexHtmlTransform; +}, extensions?: { + buildPlugins?: Plugin[]; + middleware?: ((req: http.IncomingMessage, res: http.ServerResponse, next: (err?: unknown) => void) => void)[]; + builderSelector?: (info: BuilderSelectorInfo, logger: BuilderContext['logger']) => string; +}): Observable; +export declare function isEsbuildBased(builderName: string): builderName is '@angular/build:application' | '@angular-devkit/build-angular:application' | '@angular-devkit/build-angular:browser-esbuild'; +interface BuilderSelectorInfo { + builderName: string; + forceEsbuild: boolean; +} +export {}; diff --git a/src/builders/dev-server/builder.js b/src/builders/dev-server/builder.js new file mode 100644 index 000000000..6a1ee0239 --- /dev/null +++ b/src/builders/dev-server/builder.js @@ -0,0 +1,168 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = execute; +exports.isEsbuildBased = isEsbuildBased; +const private_1 = require("@angular/build/private"); +const rxjs_1 = require("rxjs"); +const options_1 = require("./options"); +/** + * A Builder that executes a development server based on the provided browser target option. + * + * Usage of the `transforms` and/or `extensions` parameters is NOT supported and may cause + * unexpected build output or build failures. + * + * @param options Dev Server options. + * @param context The build context. + * @param transforms A map of transforms that can be used to hook into some logic (such as + * transforming webpack configuration before passing it to webpack). + * @param extensions An optional object containing an array of build plugins (esbuild-based) + * and/or HTTP request middleware. + * + * @experimental Direct usage of this function is considered experimental. + */ +function execute(options, context, transforms = {}, extensions) { + // Determine project name from builder context target + const projectName = context.target?.project; + if (!projectName) { + context.logger.error(`The "dev-server" builder requires a target to be specified.`); + return rxjs_1.EMPTY; + } + return (0, rxjs_1.defer)(() => initialize(options, projectName, context, extensions?.builderSelector)).pipe((0, rxjs_1.switchMap)(({ builderName, normalizedOptions }) => { + // Use vite-based development server for esbuild-based builds + if (isEsbuildBased(builderName)) { + if (transforms?.logging || transforms?.webpackConfiguration) { + throw new Error(`The "application" and "browser-esbuild" builders do not support Webpack transforms.`); + } + if (options.publicHost) { + context.logger.warn(`The "publicHost" option will not be used because it is not supported by the "${builderName}" builder.`); + } + if (options.disableHostCheck) { + context.logger.warn(`The "disableHostCheck" option will not be used because it is not supported by the "${builderName}" builder.`); + } + // New build system defaults hmr option to the value of liveReload + normalizedOptions.hmr ??= normalizedOptions.liveReload; + // New build system uses Vite's allowedHost option convention of true for disabling host checks + if (normalizedOptions.disableHostCheck) { + normalizedOptions.allowedHosts = true; + } + else { + normalizedOptions.allowedHosts ??= []; + } + return (0, rxjs_1.defer)(() => Promise.all([Promise.resolve().then(() => __importStar(require('@angular/build/private'))), Promise.resolve().then(() => __importStar(require('../browser-esbuild')))])).pipe((0, rxjs_1.switchMap)(([{ serveWithVite, buildApplicationInternal }, { convertBrowserOptions }]) => serveWithVite(normalizedOptions, builderName, (options, context, codePlugins) => { + return builderName === '@angular-devkit/build-angular:browser-esbuild' + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + buildApplicationInternal(convertBrowserOptions(options), context, { + codePlugins, + }) + : buildApplicationInternal(options, context, { codePlugins }); + }, context, transforms, extensions))); + } + // Warn if the initial options provided by the user enable prebundling with Webpack-based builders + if (options.prebundle) { + context.logger.warn(`Prebundling has been configured but will not be used because it is not supported by the "${builderName}" builder.`); + } + if (extensions?.buildPlugins?.length) { + throw new Error('Only the "application" and "browser-esbuild" builders support plugins.'); + } + if (extensions?.middleware?.length) { + throw new Error('Only the "application" and "browser-esbuild" builders support middleware.'); + } + // Webpack based build systems default to false for hmr option + normalizedOptions.hmr ??= false; + // Use Webpack for all other browser targets + return (0, rxjs_1.defer)(() => Promise.resolve().then(() => __importStar(require('./webpack-server')))).pipe((0, rxjs_1.switchMap)(({ serveWebpackBrowser }) => serveWebpackBrowser(normalizedOptions, builderName, context, transforms))); + })); +} +async function initialize(initialOptions, projectName, context, builderSelector = defaultBuilderSelector) { + // Purge old build disk cache. + await (0, private_1.purgeStaleBuildCache)(context); + const normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, initialOptions); + const builderName = builderSelector({ + builderName: await context.getBuilderNameForTarget(normalizedOptions.buildTarget), + forceEsbuild: !!normalizedOptions.forceEsbuild, + }, context.logger); + if (!normalizedOptions.disableHostCheck && + !/^127\.\d+\.\d+\.\d+/g.test(normalizedOptions.host) && + normalizedOptions.host !== 'localhost') { + context.logger.warn(` +Warning: This is a simple server for use in testing or debugging Angular applications +locally. It hasn't been reviewed for security issues. + +Binding this server to an open connection can result in compromising your application or +computer. Using a different host than the one passed to the "--host" flag might result in +websocket connection issues. You might need to use "--disable-host-check" if that's the +case. + `); + } + if (normalizedOptions.disableHostCheck) { + context.logger.warn('Warning: Running a server with --disable-host-check is a security risk. ' + + 'See https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a for more information.'); + } + normalizedOptions.port = await (0, private_1.checkPort)(normalizedOptions.port, normalizedOptions.host); + return { + builderName, + normalizedOptions, + }; +} +function isEsbuildBased(builderName) { + if (builderName === '@angular/build:application' || + builderName === '@angular-devkit/build-angular:application' || + builderName === '@angular-devkit/build-angular:browser-esbuild') { + return true; + } + return false; +} +function defaultBuilderSelector(info, logger) { + if (isEsbuildBased(info.builderName)) { + return info.builderName; + } + if (info.forceEsbuild) { + if (!info.builderName.startsWith('@angular-devkit/build-angular:')) { + logger.warn('Warning: Forcing the use of the esbuild-based build system with third-party builders' + + ' may cause unexpected behavior and/or build failures.'); + } + // The compatibility builder should be used if esbuild is force enabled. + return '@angular-devkit/build-angular:browser-esbuild'; + } + return info.builderName; +} diff --git a/src/builders/dev-server/index.d.ts b/src/builders/dev-server/index.d.ts new file mode 100644 index 000000000..8cb1089a0 --- /dev/null +++ b/src/builders/dev-server/index.d.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { DevServerBuilderOutput } from '@angular/build'; +import { execute } from './builder'; +import { Schema as DevServerBuilderOptions } from './schema'; +export { type DevServerBuilderOptions, type DevServerBuilderOutput, execute as executeDevServerBuilder, }; +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; +export { execute as executeDevServer }; diff --git a/src/builders/dev-server/index.js b/src/builders/dev-server/index.js new file mode 100644 index 000000000..7c7352ceb --- /dev/null +++ b/src/builders/dev-server/index.js @@ -0,0 +1,15 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.executeDevServer = exports.executeDevServerBuilder = void 0; +const architect_1 = require("@angular-devkit/architect"); +const builder_1 = require("./builder"); +Object.defineProperty(exports, "executeDevServerBuilder", { enumerable: true, get: function () { return builder_1.execute; } }); +Object.defineProperty(exports, "executeDevServer", { enumerable: true, get: function () { return builder_1.execute; } }); +exports.default = (0, architect_1.createBuilder)(builder_1.execute); diff --git a/src/builders/dev-server/options.d.ts b/src/builders/dev-server/options.d.ts new file mode 100644 index 000000000..6477b78d2 --- /dev/null +++ b/src/builders/dev-server/options.d.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { BuilderContext } from '@angular-devkit/architect'; +import { Schema as DevServerOptions } from './schema'; +export type NormalizedDevServerOptions = Awaited>; +/** + * Normalize the user provided options by creating full paths for all path based options + * and converting multi-form options into a single form that can be directly used + * by the build process. + * + * @param context The context for current builder execution. + * @param projectName The name of the project for the current execution. + * @param options An object containing the options to use for the build. + * @returns An object containing normalized options required to perform the build. + */ +export declare function normalizeOptions(context: BuilderContext, projectName: string, options: DevServerOptions): Promise<{ + buildTarget: import("@angular-devkit/architect").Target; + host: string; + port: number; + poll: number | undefined; + open: boolean | undefined; + verbose: boolean | undefined; + watch: boolean | undefined; + liveReload: boolean; + hmr: boolean | undefined; + headers: { + [key: string]: string; + } | undefined; + workspaceRoot: string; + projectRoot: string; + cacheOptions: import("../../utils/normalize-cache").NormalizedCachedOptions; + allowedHosts: string[] | undefined; + disableHostCheck: boolean | undefined; + proxyConfig: string | undefined; + servePath: string | undefined; + publicHost: string | undefined; + ssl: boolean | undefined; + sslCert: string | undefined; + sslKey: string | undefined; + forceEsbuild: boolean | undefined; + prebundle: import("./schema").PrebundleUnion; + inspect: boolean | { + host?: string; + port?: number; + }; +}>; diff --git a/src/builders/dev-server/options.js b/src/builders/dev-server/options.js new file mode 100644 index 000000000..619b7bdcf --- /dev/null +++ b/src/builders/dev-server/options.js @@ -0,0 +1,107 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeOptions = normalizeOptions; +const architect_1 = require("@angular-devkit/architect"); +const node_path_1 = __importDefault(require("node:path")); +const normalize_cache_1 = require("../../utils/normalize-cache"); +const normalize_optimization_1 = require("../../utils/normalize-optimization"); +const builder_1 = require("./builder"); +/** + * Normalize the user provided options by creating full paths for all path based options + * and converting multi-form options into a single form that can be directly used + * by the build process. + * + * @param context The context for current builder execution. + * @param projectName The name of the project for the current execution. + * @param options An object containing the options to use for the build. + * @returns An object containing normalized options required to perform the build. + */ +async function normalizeOptions(context, projectName, options) { + const { workspaceRoot, logger } = context; + const projectMetadata = await context.getProjectMetadata(projectName); + const projectRoot = node_path_1.default.join(workspaceRoot, projectMetadata.root ?? ''); + const cacheOptions = (0, normalize_cache_1.normalizeCacheOptions)(projectMetadata, workspaceRoot); + // Target specifier defaults to the current project's build target using a development configuration + const buildTargetSpecifier = options.buildTarget ?? `::development`; + const buildTarget = (0, architect_1.targetFromTargetString)(buildTargetSpecifier, projectName, 'build'); + // Get the application builder options. + const browserBuilderName = await context.getBuilderNameForTarget(buildTarget); + const rawBuildOptions = await context.getTargetOptions(buildTarget); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const buildOptions = (await context.validateOptions(rawBuildOptions, browserBuilderName)); + const optimization = (0, normalize_optimization_1.normalizeOptimization)(buildOptions.optimization); + if (options.prebundle !== false && (0, builder_1.isEsbuildBased)(browserBuilderName)) { + if (!cacheOptions.enabled) { + // Warn if the initial options provided by the user enable prebundling but caching is disabled + logger.warn('Prebundling has been configured but will not be used because caching has been disabled.'); + } + else if (optimization.scripts) { + // Warn if the initial options provided by the user enable prebundling but script optimization is enabled. + logger.warn('Prebundling has been configured but will not be used because scripts optimization is enabled.'); + } + } + let inspect = false; + const inspectRaw = options.inspect; + if (inspectRaw === true || inspectRaw === '' || inspectRaw === 'true') { + inspect = { + host: undefined, + port: undefined, + }; + } + else if (typeof inspectRaw === 'string' && inspectRaw !== 'false') { + const port = +inspectRaw; + if (isFinite(port)) { + inspect = { + host: undefined, + port, + }; + } + else { + const [host, port] = inspectRaw.split(':'); + inspect = { + host, + port: isNaN(+port) ? undefined : +port, + }; + } + } + // Initial options to keep + const { host, port, poll, open, verbose, watch, allowedHosts, disableHostCheck, liveReload, hmr, headers, proxyConfig, servePath, publicHost, ssl, sslCert, sslKey, forceEsbuild, prebundle, } = options; + // Return all the normalized options + return { + buildTarget, + host: host ?? 'localhost', + port: port ?? 4200, + poll, + open, + verbose, + watch, + liveReload: !!liveReload, + hmr, + headers, + workspaceRoot, + projectRoot, + cacheOptions, + allowedHosts, + disableHostCheck, + proxyConfig, + servePath, + publicHost, + ssl, + sslCert, + sslKey, + forceEsbuild, + // Prebundling defaults to true but requires caching to function + prebundle: cacheOptions.enabled && !optimization.scripts && (prebundle ?? true), + inspect, + }; +} diff --git a/src/builders/dev-server/schema.d.ts b/src/builders/dev-server/schema.d.ts new file mode 100644 index 000000000..4b2051529 --- /dev/null +++ b/src/builders/dev-server/schema.d.ts @@ -0,0 +1,118 @@ +/** + * Dev Server target options for Build Facade. + */ +export type Schema = { + /** + * List of hosts that are allowed to access the dev server. + */ + allowedHosts?: string[]; + /** + * A build builder target to serve in the format of `project:target[:configuration]`. You + * can also pass in more than one configuration name as a comma-separated list. Example: + * `project:target:production,staging`. + */ + buildTarget: string; + /** + * Don't verify connected clients are part of allowed hosts. + */ + disableHostCheck?: boolean; + /** + * Force the development server to use the 'browser-esbuild' builder when building. + */ + forceEsbuild?: boolean; + /** + * Custom HTTP headers to be added to all responses. + */ + headers?: { + [key: string]: string; + }; + /** + * Enable hot module replacement. + */ + hmr?: boolean; + /** + * Host to listen on. + */ + host?: string; + /** + * Activate debugging inspector. This option only has an effect when 'SSR' or 'SSG' are + * enabled. + */ + inspect?: Inspect; + /** + * Whether to reload the page on change, using live-reload. + */ + liveReload?: boolean; + /** + * Opens the url in default browser. + */ + open?: boolean; + /** + * Enable and define the file watching poll time period in milliseconds. + */ + poll?: number; + /** + * Port to listen on. + */ + port?: number; + /** + * Enable and control the Vite-based development server's prebundling capabilities. To + * enable prebundling, the Angular CLI cache must also be enabled. This option has no effect + * when using the 'browser' or other Webpack-based builders. + */ + prebundle?: PrebundleUnion; + /** + * Proxy configuration file. For more information, see + * https://angular.dev/tools/cli/serve#proxying-to-a-backend-server. + */ + proxyConfig?: string; + /** + * The URL that the browser client (or live-reload client, if enabled) should use to connect + * to the development server. Use for a complex dev server setup, such as one with reverse + * proxies. This option has no effect when using the 'application' or other esbuild-based + * builders. + */ + publicHost?: string; + /** + * The pathname where the application will be served. + */ + servePath?: string; + /** + * Serve using HTTPS. + */ + ssl?: boolean; + /** + * SSL certificate to use for serving HTTPS. + */ + sslCert?: string; + /** + * SSL key to use for serving HTTPS. + */ + sslKey?: string; + /** + * Adds more details to output logging. + */ + verbose?: boolean; + /** + * Rebuild on change. + */ + watch?: boolean; +}; +/** + * Activate debugging inspector. This option only has an effect when 'SSR' or 'SSG' are + * enabled. + */ +export type Inspect = boolean | string; +/** + * Enable and control the Vite-based development server's prebundling capabilities. To + * enable prebundling, the Angular CLI cache must also be enabled. This option has no effect + * when using the 'browser' or other Webpack-based builders. + */ +export type PrebundleUnion = boolean | PrebundleClass; +export type PrebundleClass = { + /** + * List of package imports that should not be prebundled by the development server. The + * packages will be bundled into the application code itself. + */ + exclude: string[]; +}; diff --git a/src/dev-server/schema.js b/src/builders/dev-server/schema.js similarity index 100% rename from src/dev-server/schema.js rename to src/builders/dev-server/schema.js diff --git a/src/builders/dev-server/schema.json b/src/builders/dev-server/schema.json new file mode 100644 index 000000000..495f244b1 --- /dev/null +++ b/src/builders/dev-server/schema.json @@ -0,0 +1,135 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Dev Server Target", + "description": "Dev Server target options for Build Facade.", + "type": "object", + "properties": { + "buildTarget": { + "type": "string", + "description": "A build builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", + "pattern": "^[^:\\s]*:[^:\\s]*(:[^\\s]+)?$" + }, + "port": { + "type": "number", + "description": "Port to listen on.", + "default": 4200 + }, + "host": { + "type": "string", + "description": "Host to listen on.", + "default": "localhost" + }, + "proxyConfig": { + "type": "string", + "description": "Proxy configuration file. For more information, see https://angular.dev/tools/cli/serve#proxying-to-a-backend-server." + }, + "ssl": { + "type": "boolean", + "description": "Serve using HTTPS.", + "default": false + }, + "sslKey": { + "type": "string", + "description": "SSL key to use for serving HTTPS." + }, + "sslCert": { + "type": "string", + "description": "SSL certificate to use for serving HTTPS." + }, + "headers": { + "type": "object", + "description": "Custom HTTP headers to be added to all responses.", + "propertyNames": { + "pattern": "^[-_A-Za-z0-9]+$" + }, + "additionalProperties": { + "type": "string" + } + }, + "open": { + "type": "boolean", + "description": "Opens the url in default browser.", + "default": false, + "alias": "o" + }, + "verbose": { + "type": "boolean", + "description": "Adds more details to output logging." + }, + "liveReload": { + "type": "boolean", + "description": "Whether to reload the page on change, using live-reload.", + "default": true + }, + "publicHost": { + "type": "string", + "description": "The URL that the browser client (or live-reload client, if enabled) should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies. This option has no effect when using the 'application' or other esbuild-based builders." + }, + "allowedHosts": { + "type": "array", + "description": "List of hosts that are allowed to access the dev server.", + "default": [], + "items": { + "type": "string" + } + }, + "servePath": { + "type": "string", + "description": "The pathname where the application will be served." + }, + "disableHostCheck": { + "type": "boolean", + "description": "Don't verify connected clients are part of allowed hosts.", + "default": false + }, + "hmr": { + "type": "boolean", + "description": "Enable hot module replacement." + }, + "watch": { + "type": "boolean", + "description": "Rebuild on change.", + "default": true + }, + "poll": { + "type": "number", + "description": "Enable and define the file watching poll time period in milliseconds." + }, + "inspect": { + "default": false, + "description": "Activate debugging inspector. This option only has an effect when 'SSR' or 'SSG' are enabled.", + "oneOf": [ + { + "type": "string", + "description": "Activate the inspector on host and port in the format of `[[host:]port]`. See the security warning in https://nodejs.org/docs/latest-v22.x/api/cli.html#warning-binding-inspector-to-a-public-ipport-combination-is-insecure regarding the host parameter usage." + }, + { "type": "boolean" } + ] + }, + "forceEsbuild": { + "type": "boolean", + "description": "Force the development server to use the 'browser-esbuild' builder when building.", + "default": false + }, + "prebundle": { + "description": "Enable and control the Vite-based development server's prebundling capabilities. To enable prebundling, the Angular CLI cache must also be enabled. This option has no effect when using the 'browser' or other Webpack-based builders.", + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "properties": { + "exclude": { + "description": "List of package imports that should not be prebundled by the development server. The packages will be bundled into the application code itself.", + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": false, + "required": ["exclude"] + } + ] + } + }, + "additionalProperties": false, + "required": ["buildTarget"] +} diff --git a/src/builders/dev-server/webpack-server.d.ts b/src/builders/dev-server/webpack-server.d.ts new file mode 100644 index 000000000..1062835c8 --- /dev/null +++ b/src/builders/dev-server/webpack-server.d.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { IndexHtmlTransform } from '@angular/build/private'; +import { BuilderContext } from '@angular-devkit/architect'; +import { DevServerBuildOutput, WebpackLoggingCallback } from '@angular-devkit/build-webpack'; +import { Observable } from 'rxjs'; +import webpack from 'webpack'; +import { BuildEventStats } from '../../tools/webpack/utils/stats'; +import { ExecutionTransformer } from '../../transforms'; +import { NormalizedDevServerOptions } from './options'; +/** + * @experimental Direct usage of this type is considered experimental. + */ +export type DevServerBuilderOutput = DevServerBuildOutput & { + baseUrl: string; + stats: BuildEventStats; +}; +/** + * Reusable implementation of the Angular Webpack development server builder. + * @param options Dev Server options. + * @param builderName The name of the builder used to build the application. + * @param context The build context. + * @param transforms A map of transforms that can be used to hook into some logic (such as + * transforming webpack configuration before passing it to webpack). + */ +export declare function serveWebpackBrowser(options: NormalizedDevServerOptions, builderName: string, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; + logging?: WebpackLoggingCallback; + indexHtml?: IndexHtmlTransform; +}): Observable; diff --git a/src/builders/dev-server/webpack-server.js b/src/builders/dev-server/webpack-server.js new file mode 100644 index 000000000..b1f4edc27 --- /dev/null +++ b/src/builders/dev-server/webpack-server.js @@ -0,0 +1,296 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.serveWebpackBrowser = serveWebpackBrowser; +const private_1 = require("@angular/build/private"); +const build_webpack_1 = require("@angular-devkit/build-webpack"); +const core_1 = require("@angular-devkit/core"); +const path = __importStar(require("node:path")); +const url = __importStar(require("node:url")); +const rxjs_1 = require("rxjs"); +const configs_1 = require("../../tools/webpack/configs"); +const index_html_webpack_plugin_1 = require("../../tools/webpack/plugins/index-html-webpack-plugin"); +const service_worker_plugin_1 = require("../../tools/webpack/plugins/service-worker-plugin"); +const stats_1 = require("../../tools/webpack/utils/stats"); +const utils_1 = require("../../utils"); +const color_1 = require("../../utils/color"); +const i18n_webpack_1 = require("../../utils/i18n-webpack"); +const load_esm_1 = require("../../utils/load-esm"); +const package_chunk_sort_1 = require("../../utils/package-chunk-sort"); +const webpack_browser_config_1 = require("../../utils/webpack-browser-config"); +const webpack_diagnostics_1 = require("../../utils/webpack-diagnostics"); +const schema_1 = require("../browser/schema"); +/** + * Reusable implementation of the Angular Webpack development server builder. + * @param options Dev Server options. + * @param builderName The name of the builder used to build the application. + * @param context The build context. + * @param transforms A map of transforms that can be used to hook into some logic (such as + * transforming webpack configuration before passing it to webpack). + */ +// eslint-disable-next-line max-lines-per-function +function serveWebpackBrowser(options, builderName, context, transforms = {}) { + // Check Angular version. + const { logger, workspaceRoot } = context; + (0, private_1.assertCompatibleAngularVersion)(workspaceRoot); + async function setup() { + if (options.hmr) { + logger.warn(core_1.tags.stripIndents `NOTICE: Hot Module Replacement (HMR) is enabled for the dev server. + See https://webpack.js.org/guides/hot-module-replacement for information on working with HMR for Webpack.`); + } + // Get the browser configuration from the target name. + const rawBrowserOptions = await context.getTargetOptions(options.buildTarget); + if (rawBrowserOptions.outputHashing && rawBrowserOptions.outputHashing !== schema_1.OutputHashing.None) { + // Disable output hashing for dev build as this can cause memory leaks + // See: https://github.com/webpack/webpack-dev-server/issues/377#issuecomment-241258405 + rawBrowserOptions.outputHashing = schema_1.OutputHashing.None; + logger.warn(`Warning: 'outputHashing' option is disabled when using the dev-server.`); + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const browserOptions = (await context.validateOptions({ + ...rawBrowserOptions, + watch: options.watch, + verbose: options.verbose, + // In dev server we should not have budgets because of extra libs such as socks-js + budgets: undefined, + }, builderName)); + const { styles, scripts } = (0, utils_1.normalizeOptimization)(browserOptions.optimization); + if (scripts || styles.minify) { + logger.error(core_1.tags.stripIndents ` + **************************************************************************************** + This is a simple server for use in testing or debugging Angular applications locally. + It hasn't been reviewed for security issues. + + DON'T USE IT FOR PRODUCTION! + **************************************************************************************** + `); + } + const { config, i18n } = await (0, webpack_browser_config_1.generateI18nBrowserWebpackConfigFromContext)(browserOptions, context, (wco) => [(0, configs_1.getDevServerConfig)(wco), (0, configs_1.getCommonConfig)(wco), (0, configs_1.getStylesConfig)(wco)], options); + if (!config.devServer) { + throw new Error('Webpack Dev Server configuration was not set.'); + } + let locale; + if (i18n.shouldInline) { + // Dev-server only supports one locale + locale = [...i18n.inlineLocales][0]; + } + else if (i18n.hasDefinedSourceLocale) { + // use source locale if not localizing + locale = i18n.sourceLocale; + } + let webpackConfig = config; + // If a locale is defined, setup localization + if (locale) { + if (i18n.inlineLocales.size > 1) { + throw new Error('The development server only supports localizing a single locale per build.'); + } + await setupLocalize(locale, i18n, browserOptions, webpackConfig, options.cacheOptions, context); + } + if (transforms.webpackConfiguration) { + webpackConfig = await transforms.webpackConfiguration(webpackConfig); + } + webpackConfig.plugins ??= []; + if (browserOptions.index) { + const { scripts = [], styles = [], baseHref } = browserOptions; + const entrypoints = (0, package_chunk_sort_1.generateEntryPoints)({ + scripts, + styles, + // The below is needed as otherwise HMR for CSS will break. + // styles.js and runtime.js needs to be loaded as a non-module scripts as otherwise `document.currentScript` will be null. + // https://github.com/webpack-contrib/mini-css-extract-plugin/blob/90445dd1d81da0c10b9b0e8a17b417d0651816b8/src/hmr/hotModuleReplacement.js#L39 + isHMREnabled: !!webpackConfig.devServer?.hot, + }); + webpackConfig.plugins.push(new index_html_webpack_plugin_1.IndexHtmlWebpackPlugin({ + indexPath: path.resolve(workspaceRoot, (0, webpack_browser_config_1.getIndexInputFile)(browserOptions.index)), + outputPath: (0, webpack_browser_config_1.getIndexOutputFile)(browserOptions.index), + baseHref, + entrypoints, + deployUrl: browserOptions.deployUrl, + sri: browserOptions.subresourceIntegrity, + cache: options.cacheOptions, + postTransform: transforms.indexHtml, + optimization: (0, utils_1.normalizeOptimization)(browserOptions.optimization), + crossOrigin: browserOptions.crossOrigin, + lang: locale, + })); + } + if (browserOptions.serviceWorker) { + webpackConfig.plugins.push(new service_worker_plugin_1.ServiceWorkerPlugin({ + baseHref: browserOptions.baseHref, + root: context.workspaceRoot, + projectRoot: options.projectRoot, + ngswConfigPath: browserOptions.ngswConfigPath, + })); + } + return { + browserOptions, + webpackConfig, + }; + } + return (0, rxjs_1.from)(setup()).pipe((0, rxjs_1.switchMap)(({ browserOptions, webpackConfig }) => { + return (0, build_webpack_1.runWebpackDevServer)(webpackConfig, context, { + logging: transforms.logging || (0, stats_1.createWebpackLoggingCallback)(browserOptions, logger), + webpackFactory: require('webpack'), + webpackDevServerFactory: require('webpack-dev-server'), + }).pipe((0, rxjs_1.concatMap)(async (buildEvent, index) => { + const webpackRawStats = buildEvent.webpackStats; + if (!webpackRawStats) { + throw new Error('Webpack stats build result is required.'); + } + // Resolve serve address. + const publicPath = webpackConfig.devServer?.devMiddleware?.publicPath; + const serverAddress = url.format({ + protocol: options.ssl ? 'https' : 'http', + hostname: options.host === '0.0.0.0' ? 'localhost' : options.host, + port: buildEvent.port, + pathname: typeof publicPath === 'string' ? publicPath : undefined, + }); + if (index === 0) { + logger.info('\n' + + core_1.tags.oneLine ` + ** + Angular Live Development Server is listening on ${options.host}:${buildEvent.port}, + open your browser on ${serverAddress} + ** + ` + + '\n'); + if (options.open) { + const open = (await (0, load_esm_1.loadEsmModule)('open')).default; + await open(serverAddress); + } + } + if (buildEvent.success) { + logger.info(`\n${color_1.colors.greenBright(color_1.colors.symbols.check)} Compiled successfully.`); + } + else { + logger.info(`\n${color_1.colors.redBright(color_1.colors.symbols.cross)} Failed to compile.`); + } + return { + ...buildEvent, + baseUrl: serverAddress, + stats: (0, stats_1.generateBuildEventStats)(webpackRawStats, browserOptions), + }; + })); + })); +} +async function setupLocalize(locale, i18n, browserOptions, webpackConfig, cacheOptions, context) { + const localeDescription = i18n.locales[locale]; + // Modify main entrypoint to include locale data + if (localeDescription?.dataPath && + typeof webpackConfig.entry === 'object' && + !Array.isArray(webpackConfig.entry) && + webpackConfig.entry['main']) { + if (Array.isArray(webpackConfig.entry['main'])) { + webpackConfig.entry['main'].unshift(localeDescription.dataPath); + } + else { + webpackConfig.entry['main'] = [ + localeDescription.dataPath, + webpackConfig.entry['main'], + ]; + } + } + let missingTranslationBehavior = browserOptions.i18nMissingTranslation || 'ignore'; + let translation = localeDescription?.translation || {}; + if (locale === i18n.sourceLocale) { + missingTranslationBehavior = 'ignore'; + translation = {}; + } + const i18nLoaderOptions = { + locale, + missingTranslationBehavior, + translation: i18n.shouldInline ? translation : undefined, + translationFiles: localeDescription?.files.map((file) => path.resolve(context.workspaceRoot, file.path)), + }; + const i18nRule = { + test: /\.[cm]?[tj]sx?$/, + enforce: 'post', + use: [ + { + loader: require.resolve('../../tools/babel/webpack-loader'), + options: { + cacheDirectory: (cacheOptions.enabled && path.join(cacheOptions.path, 'babel-dev-server-i18n')) || + false, + cacheIdentifier: JSON.stringify({ + locale, + translationIntegrity: localeDescription?.files.map((file) => file.integrity), + }), + i18n: i18nLoaderOptions, + }, + }, + ], + }; + // Get the rules and ensure the Webpack configuration is setup properly + const rules = webpackConfig.module?.rules || []; + if (!webpackConfig.module) { + webpackConfig.module = { rules }; + } + else if (!webpackConfig.module.rules) { + webpackConfig.module.rules = rules; + } + rules.push(i18nRule); + // Add a plugin to reload translation files on rebuilds + const loader = await (0, private_1.createTranslationLoader)(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + webpackConfig.plugins.push({ + apply: (compiler) => { + compiler.hooks.thisCompilation.tap('build-angular', (compilation) => { + if (i18n.shouldInline && i18nLoaderOptions.translation === undefined) { + // Reload translations + (0, i18n_webpack_1.loadTranslations)(locale, localeDescription, context.workspaceRoot, loader, { + warn(message) { + (0, webpack_diagnostics_1.addWarning)(compilation, message); + }, + error(message) { + (0, webpack_diagnostics_1.addError)(compilation, message); + }, + }, undefined, browserOptions.i18nDuplicateTranslation); + i18nLoaderOptions.translation = localeDescription.translation ?? {}; + } + compilation.hooks.finishModules.tap('build-angular', () => { + // After loaders are finished, clear out the now unneeded translations + i18nLoaderOptions.translation = undefined; + }); + }); + }, + }); +} diff --git a/src/builders/extract-i18n/application-extraction.d.ts b/src/builders/extract-i18n/application-extraction.d.ts new file mode 100644 index 000000000..2ce594f0b --- /dev/null +++ b/src/builders/extract-i18n/application-extraction.d.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { ɵParsedMessage as LocalizeMessage } from '@angular/localize'; +import type { MessageExtractor } from '@angular/localize/tools'; +import type { BuilderContext } from '@angular-devkit/architect'; +import type { NormalizedExtractI18nOptions } from './options'; +export declare function extractMessages(options: NormalizedExtractI18nOptions, builderName: string, context: BuilderContext, extractorConstructor: typeof MessageExtractor): Promise<{ + success: boolean; + basePath: string; + messages: LocalizeMessage[]; + useLegacyIds: boolean; +}>; diff --git a/src/builders/extract-i18n/application-extraction.js b/src/builders/extract-i18n/application-extraction.js new file mode 100644 index 000000000..34e26e990 --- /dev/null +++ b/src/builders/extract-i18n/application-extraction.js @@ -0,0 +1,135 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.extractMessages = extractMessages; +const private_1 = require("@angular/build/private"); +const node_fs_1 = require("node:fs"); +const node_path_1 = __importDefault(require("node:path")); +const browser_esbuild_1 = require("../browser-esbuild"); +async function extractMessages(options, builderName, context, extractorConstructor) { + const messages = []; + // Setup the build options for the application based on the buildTarget option + let buildOptions; + if (builderName === '@angular-devkit/build-angular:application') { + buildOptions = (await context.validateOptions(await context.getTargetOptions(options.buildTarget), builderName)); + } + else { + buildOptions = (0, browser_esbuild_1.convertBrowserOptions)((await context.validateOptions(await context.getTargetOptions(options.buildTarget), builderName))); + } + buildOptions.optimization = false; + buildOptions.sourceMap = { scripts: true, vendor: true, styles: false }; + buildOptions.localize = false; + buildOptions.budgets = undefined; + buildOptions.index = false; + buildOptions.serviceWorker = false; + buildOptions.server = undefined; + buildOptions.ssr = false; + buildOptions.appShell = undefined; + buildOptions.prerender = undefined; + buildOptions.outputMode = undefined; + const builderResult = await first((0, private_1.buildApplicationInternal)(buildOptions, context)); + let success = false; + if (!builderResult || builderResult.kind === private_1.ResultKind.Failure) { + context.logger.error('Application build failed.'); + } + else if (builderResult.kind !== private_1.ResultKind.Full) { + context.logger.error('Application build did not provide a full output.'); + } + else { + // Setup the localize message extractor based on the in-memory files + const extractor = setupLocalizeExtractor(extractorConstructor, builderResult.files, context); + // Extract messages from each output JavaScript file. + // Output files are only present on a successful build. + for (const filePath of Object.keys(builderResult.files)) { + if (!filePath.endsWith('.js')) { + continue; + } + const fileMessages = extractor.extractMessages(filePath); + messages.push(...fileMessages); + } + success = true; + } + return { + success, + basePath: context.workspaceRoot, + messages, + // Legacy i18n identifiers are not supported with the new application builder + useLegacyIds: false, + }; +} +function setupLocalizeExtractor(extractorConstructor, files, context) { + const textDecoder = new TextDecoder(); + // Setup a virtual file system instance for the extractor + // * MessageExtractor itself uses readFile, relative and resolve + // * Internal SourceFileLoader (sourcemap support) uses dirname, exists, readFile, and resolve + const filesystem = { + readFile(path) { + // Output files are stored as relative to the workspace root + const requestedPath = node_path_1.default.relative(context.workspaceRoot, path); + const file = files[requestedPath]; + let content; + if (file?.origin === 'memory') { + content = textDecoder.decode(file.contents); + } + else if (file?.origin === 'disk') { + content = (0, node_fs_1.readFileSync)(file.inputPath, 'utf-8'); + } + if (content === undefined) { + throw new Error('Unknown file requested: ' + requestedPath); + } + return content; + }, + relative(from, to) { + return node_path_1.default.relative(from, to); + }, + resolve(...paths) { + return node_path_1.default.resolve(...paths); + }, + exists(path) { + // Output files are stored as relative to the workspace root + const requestedPath = node_path_1.default.relative(context.workspaceRoot, path); + return files[requestedPath] !== undefined; + }, + dirname(path) { + return node_path_1.default.dirname(path); + }, + }; + const logger = { + // level 2 is warnings + level: 2, + debug(...args) { + // eslint-disable-next-line no-console + console.debug(...args); + }, + info(...args) { + context.logger.info(args.join('')); + }, + warn(...args) { + context.logger.warn(args.join('')); + }, + error(...args) { + context.logger.error(args.join('')); + }, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const extractor = new extractorConstructor(filesystem, logger, { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + basePath: context.workspaceRoot, + useSourceMaps: true, + }); + return extractor; +} +async function first(iterable) { + for await (const value of iterable) { + return value; + } +} diff --git a/src/builders/extract-i18n/builder.d.ts b/src/builders/extract-i18n/builder.d.ts new file mode 100644 index 000000000..15f05b5ba --- /dev/null +++ b/src/builders/extract-i18n/builder.d.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import type webpack from 'webpack'; +import type { ExecutionTransformer } from '../../transforms'; +import { Schema as ExtractI18nBuilderOptions } from './schema'; +/** + * @experimental Direct usage of this function is considered experimental. + */ +export declare function execute(options: ExtractI18nBuilderOptions, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; +}): Promise; diff --git a/src/builders/extract-i18n/builder.js b/src/builders/extract-i18n/builder.js new file mode 100644 index 000000000..314a31856 --- /dev/null +++ b/src/builders/extract-i18n/builder.js @@ -0,0 +1,177 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = execute; +const private_1 = require("@angular/build/private"); +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +const load_esm_1 = require("../../utils/load-esm"); +const options_1 = require("./options"); +const schema_1 = require("./schema"); +/** + * @experimental Direct usage of this function is considered experimental. + */ +async function execute(options, context, transforms) { + // Determine project name from builder context target + const projectName = context.target?.project; + if (!projectName) { + context.logger.error(`The 'extract-i18n' builder requires a target to be specified.`); + return { success: false }; + } + const { projectType } = (await context.getProjectMetadata(projectName)); + if (projectType !== 'application') { + context.logger.error(`Tried to extract from ${projectName} with 'projectType' ${projectType}, which is not supported.` + + ` The 'extract-i18n' builder can only extract from applications.`); + return { success: false }; + } + // Check Angular version. + (0, private_1.assertCompatibleAngularVersion)(context.workspaceRoot); + // Load the Angular localize package. + // The package is a peer dependency and might not be present + let localizeToolsModule; + try { + localizeToolsModule = + await (0, load_esm_1.loadEsmModule)('@angular/localize/tools'); + } + catch { + return { + success: false, + error: `i18n extraction requires the '@angular/localize' package.` + + ` You can add it by using 'ng add @angular/localize'.`, + }; + } + // Normalize options + const normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, options); + const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget); + // Extract messages based on configured builder + let extractionResult; + if (builderName === '@angular-devkit/build-angular:application' || + builderName === '@angular-devkit/build-angular:browser-esbuild') { + const { extractMessages } = await Promise.resolve().then(() => __importStar(require('./application-extraction'))); + extractionResult = await extractMessages(normalizedOptions, builderName, context, localizeToolsModule.MessageExtractor); + if (!extractionResult.success) { + return { success: false }; + } + } + else { + // Purge old build disk cache. + // Other build systems handle stale cache purging directly. + await (0, private_1.purgeStaleBuildCache)(context); + const { extractMessages } = await Promise.resolve().then(() => __importStar(require('./webpack-extraction'))); + extractionResult = await extractMessages(normalizedOptions, builderName, context, transforms); + // Return the builder result if it failed + if (!extractionResult.builderResult.success) { + return extractionResult.builderResult; + } + } + // Perform duplicate message checks + const { checkDuplicateMessages } = localizeToolsModule; + // The filesystem is used to create a relative path for each file + // from the basePath. This relative path is then used in the error message. + const checkFileSystem = { + relative(from, to) { + return node_path_1.default.relative(from, to); + }, + }; + const duplicateTranslationBehavior = normalizedOptions.i18nOptions.duplicateTranslationBehavior; + const diagnostics = checkDuplicateMessages( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + checkFileSystem, extractionResult.messages, duplicateTranslationBehavior, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extractionResult.basePath); + if (diagnostics.messages.length > 0 && duplicateTranslationBehavior !== 'ignore') { + if (duplicateTranslationBehavior === 'error') { + context.logger.error(`Extraction Failed: ${diagnostics.formatDiagnostics('')}`); + return { success: false }; + } + else { + context.logger.warn(diagnostics.formatDiagnostics('')); + } + } + // Serialize all extracted messages + const serializer = await createSerializer(localizeToolsModule, normalizedOptions.format, normalizedOptions.i18nOptions.sourceLocale, extractionResult.basePath, extractionResult.useLegacyIds, diagnostics); + const content = serializer.serialize(extractionResult.messages); + // Ensure directory exists + const outputPath = node_path_1.default.dirname(normalizedOptions.outFile); + if (!node_fs_1.default.existsSync(outputPath)) { + node_fs_1.default.mkdirSync(outputPath, { recursive: true }); + } + // Write translation file + node_fs_1.default.writeFileSync(normalizedOptions.outFile, content); + if (normalizedOptions.progress) { + context.logger.info(`Extraction Complete. (Messages: ${extractionResult.messages.length})`); + } + return { success: true, outputPath: normalizedOptions.outFile }; +} +async function createSerializer(localizeToolsModule, format, sourceLocale, basePath, useLegacyIds, diagnostics) { + const { XmbTranslationSerializer, LegacyMessageIdMigrationSerializer, ArbTranslationSerializer, Xliff1TranslationSerializer, Xliff2TranslationSerializer, SimpleJsonTranslationSerializer, } = localizeToolsModule; + switch (format) { + case schema_1.Format.Xmb: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new XmbTranslationSerializer(basePath, useLegacyIds); + case schema_1.Format.Xlf: + case schema_1.Format.Xlif: + case schema_1.Format.Xliff: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new Xliff1TranslationSerializer(sourceLocale, basePath, useLegacyIds, {}); + case schema_1.Format.Xlf2: + case schema_1.Format.Xliff2: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new Xliff2TranslationSerializer(sourceLocale, basePath, useLegacyIds, {}); + case schema_1.Format.Json: + return new SimpleJsonTranslationSerializer(sourceLocale); + case schema_1.Format.LegacyMigrate: + return new LegacyMessageIdMigrationSerializer(diagnostics); + case schema_1.Format.Arb: + return new ArbTranslationSerializer(sourceLocale, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + basePath, { + relative(from, to) { + return node_path_1.default.relative(from, to); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }); + } +} diff --git a/src/builders/extract-i18n/empty-loader.d.ts b/src/builders/extract-i18n/empty-loader.d.ts new file mode 100644 index 000000000..e60209367 --- /dev/null +++ b/src/builders/extract-i18n/empty-loader.d.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export default function (): string; diff --git a/src/builders/extract-i18n/empty-loader.js b/src/builders/extract-i18n/empty-loader.js new file mode 100644 index 000000000..342890839 --- /dev/null +++ b/src/builders/extract-i18n/empty-loader.js @@ -0,0 +1,13 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +function default_1() { + return `export default '';`; +} diff --git a/src/builders/extract-i18n/index.d.ts b/src/builders/extract-i18n/index.d.ts new file mode 100644 index 000000000..62661acd5 --- /dev/null +++ b/src/builders/extract-i18n/index.d.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { execute } from './builder'; +import type { Schema as ExtractI18nBuilderOptions } from './schema'; +export { ExtractI18nBuilderOptions, execute }; +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; diff --git a/src/builders/extract-i18n/index.js b/src/builders/extract-i18n/index.js new file mode 100644 index 000000000..a33fef33f --- /dev/null +++ b/src/builders/extract-i18n/index.js @@ -0,0 +1,14 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = void 0; +const architect_1 = require("@angular-devkit/architect"); +const builder_1 = require("./builder"); +Object.defineProperty(exports, "execute", { enumerable: true, get: function () { return builder_1.execute; } }); +exports.default = (0, architect_1.createBuilder)(builder_1.execute); diff --git a/src/builders/extract-i18n/ivy-extract-loader.d.ts b/src/builders/extract-i18n/ivy-extract-loader.d.ts new file mode 100644 index 000000000..9597a4a7d --- /dev/null +++ b/src/builders/extract-i18n/ivy-extract-loader.d.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +type LoaderSourceMap = Parameters[1]; +interface LocalizeExtractLoaderOptions { + messageHandler: (messages: import('@angular/localize').ɵParsedMessage[]) => void; +} +export default function localizeExtractLoader(this: import('webpack').LoaderContext, content: string, map: LoaderSourceMap): void; +export {}; diff --git a/src/builders/extract-i18n/ivy-extract-loader.js b/src/builders/extract-i18n/ivy-extract-loader.js new file mode 100644 index 000000000..b00ced151 --- /dev/null +++ b/src/builders/extract-i18n/ivy-extract-loader.js @@ -0,0 +1,137 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = localizeExtractLoader; +const nodePath = __importStar(require("node:path")); +const load_esm_1 = require("../../utils/load-esm"); +function localizeExtractLoader(content, map) { + // This loader is not cacheable due to how message extraction works. + // Extracted messages are not part of webpack pipeline and hence they cannot be retrieved from cache. + // TODO: We should investigate in the future on making this deterministic and more cacheable. + this.cacheable(false); + const options = this.getOptions(); + const callback = this.async(); + extract(this, content, map, options).then(() => { + // Pass through the original content now that messages have been extracted + callback(undefined, content, map); + }, (error) => { + callback(error); + }); +} +async function extract(loaderContext, content, map, options) { + // Try to load the `@angular/localize` message extractor. + // All the localize usages are setup to first try the ESM entry point then fallback to the deep imports. + // This provides interim compatibility while the framework is transitioned to bundled ESM packages. + let MessageExtractor; + try { + // Load ESM `@angular/localize/tools` using the TypeScript dynamic import workaround. + // Once TypeScript provides support for keeping the dynamic import this workaround can be + // changed to a direct dynamic import. + const localizeToolsModule = await (0, load_esm_1.loadEsmModule)('@angular/localize/tools'); + MessageExtractor = localizeToolsModule.MessageExtractor; + } + catch { + throw new Error(`Unable to load message extractor. Please ensure '@angular/localize' is installed.`); + } + // Setup a Webpack-based logger instance + const logger = { + // level 2 is warnings + level: 2, + debug(...args) { + // eslint-disable-next-line no-console + console.debug(...args); + }, + info(...args) { + loaderContext.emitWarning(new Error(args.join(''))); + }, + warn(...args) { + loaderContext.emitWarning(new Error(args.join(''))); + }, + error(...args) { + loaderContext.emitError(new Error(args.join(''))); + }, + }; + let filename = loaderContext.resourcePath; + const mapObject = typeof map === 'string' ? JSON.parse(map) : map; + if (mapObject?.file) { + // The extractor's internal sourcemap handling expects the filenames to match + filename = nodePath.join(loaderContext.context, mapObject.file); + } + // Setup a virtual file system instance for the extractor + // * MessageExtractor itself uses readFile, relative and resolve + // * Internal SourceFileLoader (sourcemap support) uses dirname, exists, readFile, and resolve + const filesystem = { + readFile(path) { + if (path === filename) { + return content; + } + else if (path === filename + '.map') { + return typeof map === 'string' ? map : JSON.stringify(map); + } + else { + throw new Error('Unknown file requested: ' + path); + } + }, + relative(from, to) { + return nodePath.relative(from, to); + }, + resolve(...paths) { + return nodePath.resolve(...paths); + }, + exists(path) { + return path === filename || path === filename + '.map'; + }, + dirname(path) { + return nodePath.dirname(path); + }, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const extractor = new MessageExtractor(filesystem, logger, { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + basePath: loaderContext.rootContext, + useSourceMaps: !!map, + }); + const messages = extractor.extractMessages(filename); + if (messages.length > 0) { + options?.messageHandler(messages); + } +} diff --git a/src/builders/extract-i18n/options.d.ts b/src/builders/extract-i18n/options.d.ts new file mode 100644 index 000000000..aba9e5308 --- /dev/null +++ b/src/builders/extract-i18n/options.d.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { type I18nOptions } from '@angular/build/private'; +import { type DiagnosticHandlingStrategy } from '@angular/localize/tools'; +import { BuilderContext } from '@angular-devkit/architect'; +import { Schema as ExtractI18nOptions, Format } from './schema'; +export type NormalizedExtractI18nOptions = Awaited>; +/** + * Normalize the user provided options by creating full paths for all path based options + * and converting multi-form options into a single form that can be directly used + * by the build process. + * + * @param context The context for current builder execution. + * @param projectName The name of the project for the current execution. + * @param options An object containing the options to use for the build. + * @returns An object containing normalized options required to perform the build. + */ +export declare function normalizeOptions(context: BuilderContext, projectName: string, options: ExtractI18nOptions): Promise<{ + workspaceRoot: string; + projectRoot: string; + buildTarget: import("@angular-devkit/architect").Target; + i18nOptions: I18nOptions & { + duplicateTranslationBehavior: DiagnosticHandlingStrategy; + }; + format: Format.Arb | Format.Json | Format.LegacyMigrate | Format.Xliff | Format.Xliff2 | Format.Xmb; + outFile: string; + progress: boolean; +}>; diff --git a/src/builders/extract-i18n/options.js b/src/builders/extract-i18n/options.js new file mode 100644 index 000000000..fc7cfbe8c --- /dev/null +++ b/src/builders/extract-i18n/options.js @@ -0,0 +1,85 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeOptions = normalizeOptions; +const private_1 = require("@angular/build/private"); +const architect_1 = require("@angular-devkit/architect"); +const node_assert_1 = require("node:assert"); +const node_path_1 = __importDefault(require("node:path")); +const schema_1 = require("./schema"); +/** + * Normalize the user provided options by creating full paths for all path based options + * and converting multi-form options into a single form that can be directly used + * by the build process. + * + * @param context The context for current builder execution. + * @param projectName The name of the project for the current execution. + * @param options An object containing the options to use for the build. + * @returns An object containing normalized options required to perform the build. + */ +async function normalizeOptions(context, projectName, options) { + const workspaceRoot = context.workspaceRoot; + const projectMetadata = await context.getProjectMetadata(projectName); + const projectRoot = node_path_1.default.join(workspaceRoot, projectMetadata.root ?? ''); + // Target specifier defaults to the current project's build target with no specified configuration + const buildTargetSpecifier = options.buildTarget ?? ':'; + const buildTarget = (0, architect_1.targetFromTargetString)(buildTargetSpecifier, projectName, 'build'); + const i18nOptions = { + ...(0, private_1.createI18nOptions)(projectMetadata, /** inline */ false, context.logger), + duplicateTranslationBehavior: options.i18nDuplicateTranslation || 'warning', + }; + // Normalize xliff format extensions + let format = options.format; + switch (format) { + // Default format is xliff1 + case undefined: + case schema_1.Format.Xlf: + case schema_1.Format.Xlif: + case schema_1.Format.Xliff: + format = schema_1.Format.Xliff; + break; + case schema_1.Format.Xlf2: + case schema_1.Format.Xliff2: + format = schema_1.Format.Xliff2; + break; + } + let outFile = options.outFile || getDefaultOutFile(format); + if (options.outputPath) { + outFile = node_path_1.default.join(options.outputPath, outFile); + } + outFile = node_path_1.default.resolve(context.workspaceRoot, outFile); + return { + workspaceRoot, + projectRoot, + buildTarget, + i18nOptions, + format, + outFile, + progress: options.progress ?? true, + }; +} +function getDefaultOutFile(format) { + switch (format) { + case schema_1.Format.Xmb: + return 'messages.xmb'; + case schema_1.Format.Xliff: + case schema_1.Format.Xliff2: + return 'messages.xlf'; + case schema_1.Format.Json: + case schema_1.Format.LegacyMigrate: + return 'messages.json'; + case schema_1.Format.Arb: + return 'messages.arb'; + default: + (0, node_assert_1.fail)(`Invalid Format enum value: ${format}`); + } +} diff --git a/src/builders/extract-i18n/schema.d.ts b/src/builders/extract-i18n/schema.d.ts new file mode 100644 index 000000000..b59112013 --- /dev/null +++ b/src/builders/extract-i18n/schema.d.ts @@ -0,0 +1,53 @@ +/** + * Extract i18n target options for Build Facade. + */ +export type Schema = { + /** + * A builder target to extract i18n messages in the format of + * `project:target[:configuration]`. You can also pass in more than one configuration name + * as a comma-separated list. Example: `project:target:production,staging`. + */ + buildTarget?: string; + /** + * Output format for the generated file. + */ + format?: Format; + /** + * How to handle duplicate translations. + */ + i18nDuplicateTranslation?: I18NDuplicateTranslation; + /** + * Name of the file to output. + */ + outFile?: string; + /** + * Path where output will be placed. + */ + outputPath?: string; + /** + * Log progress to the console. + */ + progress?: boolean; +}; +/** + * Output format for the generated file. + */ +export declare enum Format { + Arb = "arb", + Json = "json", + LegacyMigrate = "legacy-migrate", + Xlf = "xlf", + Xlf2 = "xlf2", + Xlif = "xlif", + Xliff = "xliff", + Xliff2 = "xliff2", + Xmb = "xmb" +} +/** + * How to handle duplicate translations. + */ +export declare enum I18NDuplicateTranslation { + Error = "error", + Ignore = "ignore", + Warning = "warning" +} diff --git a/src/builders/extract-i18n/schema.js b/src/builders/extract-i18n/schema.js new file mode 100644 index 000000000..3904eb681 --- /dev/null +++ b/src/builders/extract-i18n/schema.js @@ -0,0 +1,29 @@ +"use strict"; +// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE +// CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). +Object.defineProperty(exports, "__esModule", { value: true }); +exports.I18NDuplicateTranslation = exports.Format = void 0; +/** + * Output format for the generated file. + */ +var Format; +(function (Format) { + Format["Arb"] = "arb"; + Format["Json"] = "json"; + Format["LegacyMigrate"] = "legacy-migrate"; + Format["Xlf"] = "xlf"; + Format["Xlf2"] = "xlf2"; + Format["Xlif"] = "xlif"; + Format["Xliff"] = "xliff"; + Format["Xliff2"] = "xliff2"; + Format["Xmb"] = "xmb"; +})(Format || (exports.Format = Format = {})); +/** + * How to handle duplicate translations. + */ +var I18NDuplicateTranslation; +(function (I18NDuplicateTranslation) { + I18NDuplicateTranslation["Error"] = "error"; + I18NDuplicateTranslation["Ignore"] = "ignore"; + I18NDuplicateTranslation["Warning"] = "warning"; +})(I18NDuplicateTranslation || (exports.I18NDuplicateTranslation = I18NDuplicateTranslation = {})); diff --git a/src/builders/extract-i18n/schema.json b/src/builders/extract-i18n/schema.json new file mode 100644 index 000000000..08a118ad7 --- /dev/null +++ b/src/builders/extract-i18n/schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Extract i18n Target", + "description": "Extract i18n target options for Build Facade.", + "type": "object", + "properties": { + "buildTarget": { + "type": "string", + "description": "A builder target to extract i18n messages in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", + "pattern": "^[^:\\s]*:[^:\\s]*(:[^\\s]+)?$" + }, + "format": { + "type": "string", + "description": "Output format for the generated file.", + "default": "xlf", + "enum": ["xmb", "xlf", "xlif", "xliff", "xlf2", "xliff2", "json", "arb", "legacy-migrate"] + }, + "progress": { + "type": "boolean", + "description": "Log progress to the console.", + "default": true + }, + "outputPath": { + "type": "string", + "description": "Path where output will be placed." + }, + "outFile": { + "type": "string", + "description": "Name of the file to output." + }, + "i18nDuplicateTranslation": { + "type": "string", + "description": "How to handle duplicate translations.", + "enum": ["error", "warning", "ignore"] + } + }, + "additionalProperties": false +} diff --git a/src/builders/extract-i18n/webpack-extraction.d.ts b/src/builders/extract-i18n/webpack-extraction.d.ts new file mode 100644 index 000000000..82e382a7b --- /dev/null +++ b/src/builders/extract-i18n/webpack-extraction.d.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { ɵParsedMessage as LocalizeMessage } from '@angular/localize'; +import { BuilderContext } from '@angular-devkit/architect'; +import { BuildResult } from '@angular-devkit/build-webpack'; +import webpack from 'webpack'; +import { ExecutionTransformer } from '../../transforms'; +import { NormalizedExtractI18nOptions } from './options'; +export declare function extractMessages(options: NormalizedExtractI18nOptions, builderName: string, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; +}): Promise<{ + builderResult: BuildResult; + basePath: string; + messages: LocalizeMessage[]; + useLegacyIds: boolean; +}>; diff --git a/src/builders/extract-i18n/webpack-extraction.js b/src/builders/extract-i18n/webpack-extraction.js new file mode 100644 index 000000000..b35fc0913 --- /dev/null +++ b/src/builders/extract-i18n/webpack-extraction.js @@ -0,0 +1,98 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.extractMessages = extractMessages; +const build_webpack_1 = require("@angular-devkit/build-webpack"); +const rxjs_1 = require("rxjs"); +const webpack_1 = __importDefault(require("webpack")); +const configs_1 = require("../../tools/webpack/configs"); +const stats_1 = require("../../tools/webpack/utils/stats"); +const webpack_browser_config_1 = require("../../utils/webpack-browser-config"); +const schema_1 = require("../browser/schema"); +class NoEmitPlugin { + apply(compiler) { + compiler.hooks.shouldEmit.tap('angular-no-emit', () => false); + } +} +async function extractMessages(options, builderName, context, transforms = {}) { + const messages = []; + let useLegacyIds = true; + const browserOptions = await context.validateOptions(await context.getTargetOptions(options.buildTarget), builderName); + const builderOptions = { + ...browserOptions, + optimization: false, + sourceMap: { + scripts: true, + styles: false, + vendor: true, + }, + buildOptimizer: false, + aot: true, + progress: options.progress, + budgets: [], + assets: [], + scripts: [], + styles: [], + deleteOutputPath: false, + extractLicenses: false, + subresourceIntegrity: false, + outputHashing: schema_1.OutputHashing.None, + namedChunks: true, + allowedCommonJsDependencies: undefined, + }; + const { config } = await (0, webpack_browser_config_1.generateBrowserWebpackConfigFromContext)(builderOptions, context, (wco) => { + // Default value for legacy message ids is currently true + useLegacyIds = wco.tsConfig.options.enableI18nLegacyMessageIdFormat ?? true; + const partials = [ + { plugins: [new NoEmitPlugin()] }, + (0, configs_1.getCommonConfig)(wco), + ]; + // Add Ivy application file extractor support + partials.unshift({ + module: { + rules: [ + { + test: /\.[cm]?[tj]sx?$/, + loader: require.resolve('./ivy-extract-loader'), + options: { + messageHandler: (fileMessages) => messages.push(...fileMessages), + }, + }, + ], + }, + }); + // Replace all stylesheets with empty content + partials.push({ + module: { + rules: [ + { + test: /\.(css|scss|sass|less)$/, + loader: require.resolve('./empty-loader'), + }, + ], + }, + }); + return partials; + }, + // During extraction we don't need specific browser support. + { supportedBrowsers: undefined }); + const builderResult = await (0, rxjs_1.lastValueFrom)((0, build_webpack_1.runWebpack)((await transforms?.webpackConfiguration?.(config)) || config, context, { + logging: (0, stats_1.createWebpackLoggingCallback)(builderOptions, context.logger), + webpackFactory: webpack_1.default, + })); + return { + builderResult, + basePath: config.context || options.projectRoot, + messages, + useLegacyIds, + }; +} diff --git a/src/builders/jest/index.d.ts b/src/builders/jest/index.d.ts new file mode 100644 index 000000000..169d270fd --- /dev/null +++ b/src/builders/jest/index.d.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Schema as JestBuilderSchema } from './schema'; +/** Main execution function for the Jest builder. */ +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; diff --git a/src/builders/jest/index.js b/src/builders/jest/index.js new file mode 100644 index 000000000..62fd4264a --- /dev/null +++ b/src/builders/jest/index.js @@ -0,0 +1,212 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const private_1 = require("@angular/build/private"); +const architect_1 = require("@angular-devkit/architect"); +const node_child_process_1 = require("node:child_process"); +const node_crypto_1 = require("node:crypto"); +const fs = __importStar(require("node:fs/promises")); +const path = __importStar(require("node:path")); +const node_util_1 = require("node:util"); +const color_1 = require("../../utils/color"); +const test_files_1 = require("../../utils/test-files"); +const schema_1 = require("../browser-esbuild/schema"); +const write_test_files_1 = require("../web-test-runner/write-test-files"); +const options_1 = require("./options"); +const execFile = (0, node_util_1.promisify)(node_child_process_1.execFile); +/** Main execution function for the Jest builder. */ +exports.default = (0, architect_1.createBuilder)(async (schema, context) => { + context.logger.warn('NOTE: The Jest builder is currently EXPERIMENTAL and not ready for production use.'); + const options = (0, options_1.normalizeOptions)(schema); + const testOut = path.join(context.workspaceRoot, 'dist/test-out', (0, node_crypto_1.randomUUID)()); // TODO(dgp1130): Hide in temp directory. + // Verify Jest installation and get the path to it's binary. + // We need to `node_modules/.bin/jest`, but there is no means to resolve that directly. Fortunately Jest's `package.json` exports the + // same file at `bin/jest`, so we can just resolve that instead. + const jest = resolveModule('jest/bin/jest'); + if (!jest) { + return { + success: false, + // TODO(dgp1130): Display a more accurate message for non-NPM users. + error: 'Jest is not installed, most likely you need to run `npm install jest --save-dev` in your project.', + }; + } + // Verify that JSDom is installed in the project. + const environment = resolveModule('jest-environment-jsdom'); + if (!environment) { + return { + success: false, + // TODO(dgp1130): Display a more accurate message for non-NPM users. + error: '`jest-environment-jsdom` is not installed. Install it with `npm install jest-environment-jsdom --save-dev`.', + }; + } + const [testFiles, customConfig] = await Promise.all([ + (0, test_files_1.findTestFiles)(options.include, options.exclude, context.workspaceRoot), + findCustomJestConfig(context.workspaceRoot), + ]); + // Warn if a custom Jest configuration is found. We won't use it, so if a developer is trying to use a custom config, this hopefully + // makes a better experience than silently ignoring the configuration. + // Ideally, this would be a hard error. However a Jest config could exist for testing other files in the workspace outside of Angular + // CLI, so we likely can't produce a hard error in this situation without an opt-out. + if (customConfig) { + context.logger.warn('A custom Jest config was found, but this is not supported by `@angular-devkit/build-angular:jest` and will be' + + ` ignored: ${customConfig}. This is an experiment to see if completely abstracting away Jest's configuration is viable. Please` + + ` consider if your use case can be met without directly modifying the Jest config. If this is a major obstacle for your use` + + ` case, please post it in this issue so we can collect feedback and evaluate: https://github.com/angular/angular-cli/issues/25434.`); + } + // Build all the test files. + const jestGlobal = path.join(__dirname, 'jest-global.mjs'); + const initTestBed = path.join(__dirname, 'init-test-bed.mjs'); + const buildResult = await first((0, private_1.buildApplicationInternal)({ + // Build all the test files and also the `jest-global` and `init-test-bed` scripts. + entryPoints: new Set([...testFiles, jestGlobal, initTestBed]), + tsConfig: options.tsConfig, + polyfills: options.polyfills ?? ['zone.js', 'zone.js/testing'], + outputPath: testOut, + aot: options.aot, + index: false, + outputHashing: schema_1.OutputHashing.None, + outExtension: 'mjs', // Force native ESM. + optimization: false, + sourceMap: { + scripts: true, + styles: false, + vendor: false, + }, + }, context)); + if (buildResult.kind === private_1.ResultKind.Failure) { + return { success: false }; + } + else if (buildResult.kind !== private_1.ResultKind.Full) { + return { + success: false, + error: 'A full build result is required from the application builder.', + }; + } + // Write test files + await (0, write_test_files_1.writeTestFiles)(buildResult.files, testOut); + // Execute Jest on the built output directory. + const jestProc = execFile(process.execPath, [ + '--experimental-vm-modules', + jest, + `--rootDir="${testOut}"`, + `--config=${path.join(__dirname, 'jest.config.mjs')}`, + '--testEnvironment=jsdom', + // TODO(dgp1130): Enable cache once we have a mechanism for properly clearing / disabling it. + '--no-cache', + // Run basically all files in the output directory, any excluded files were already dropped by the build. + `--testMatch="/**/*.mjs"`, + // Load polyfills and initialize the environment before executing each test file. + // IMPORTANT: Order matters here. + // First, we execute `jest-global.mjs` to initialize the `jest` global variable. + // Second, we execute user polyfills, including `zone.js` and `zone.js/testing`. This is dependent on the Jest global so it can patch + // the environment for fake async to work correctly. + // Third, we initialize `TestBed`. This is dependent on fake async being set up correctly beforehand. + `--setupFilesAfterEnv="/jest-global.mjs"`, + ...(options.polyfills?.length ? [`--setupFilesAfterEnv="/polyfills.mjs"`] : []), + `--setupFilesAfterEnv="/init-test-bed.mjs"`, + // Don't run any infrastructure files as tests, they are manually loaded where needed. + `--testPathIgnorePatterns="/jest-global\\.mjs"`, + ...(options.polyfills ? [`--testPathIgnorePatterns="/polyfills\\.mjs"`] : []), + `--testPathIgnorePatterns="/init-test-bed\\.mjs"`, + // Skip shared chunks, as they are not entry points to tests. + `--testPathIgnorePatterns="/chunk-.*\\.mjs"`, + // Optionally enable color. + ...(color_1.colors.enabled ? ['--colors'] : []), + ]); + // Stream test output to the terminal. + jestProc.child.stdout?.on('data', (chunk) => { + context.logger.info(chunk); + }); + jestProc.child.stderr?.on('data', (chunk) => { + // Write to stderr directly instead of `context.logger.error(chunk)` because the logger will overwrite Jest's coloring information. + process.stderr.write(chunk); + }); + try { + await jestProc; + } + catch (error) { + // No need to propagate error message, already piped to terminal output. + // TODO(dgp1130): Handle process spawning failures. + return { success: false }; + } + return { success: true }; +}); +/** Returns the first item yielded by the given generator and cancels the execution. */ +async function first(generator) { + for await (const value of generator) { + return value; + } + throw new Error('Expected generator to emit at least once.'); +} +/** Safely resolves the given Node module string. */ +function resolveModule(module) { + try { + return require.resolve(module); + } + catch { + return undefined; + } +} +/** Returns whether or not the provided directory includes a Jest configuration file. */ +async function findCustomJestConfig(dir) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + // Jest supports many file extensions (`js`, `ts`, `cjs`, `cts`, `json`, etc.) Just look + // for anything with that prefix. + const config = entries.find((entry) => entry.isFile() && entry.name.startsWith('jest.config.')); + if (config) { + return path.join(dir, config.name); + } + // Jest also supports a `jest` key in `package.json`, look for a config there. + const packageJsonPath = path.join(dir, 'package.json'); + let packageJson; + try { + packageJson = await fs.readFile(packageJsonPath, 'utf8'); + } + catch { + return undefined; // No package.json, therefore no Jest configuration in it. + } + const json = JSON.parse(packageJson); + if ('jest' in json) { + return packageJsonPath; + } + return undefined; +} diff --git a/src/builders/jest/init-test-bed.mjs b/src/builders/jest/init-test-bed.mjs new file mode 100644 index 000000000..2a9913b70 --- /dev/null +++ b/src/builders/jest/init-test-bed.mjs @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +// TODO(dgp1130): These imports likely don't resolve in stricter package environments like `pnpm`, since they are resolved relative to +// `@angular-devkit/build-angular` rather than the user's workspace. Should look into virtual modules to support those use cases. + +import { NgModule, provideZoneChangeDetection } from '@angular/core'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; + +@NgModule({ + providers: [typeof window.Zone !== 'undefined' ? provideZoneChangeDetection() : []], +}) +class TestModule {} + +getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/src/builders/jest/jest-global.mjs b/src/builders/jest/jest-global.mjs new file mode 100644 index 000000000..40b8135b4 --- /dev/null +++ b/src/builders/jest/jest-global.mjs @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/** + * @fileoverview Zone.js requires the `jest` global to be initialized in order to know that it must patch the environment to support Jest + * execution. When running ESM code, Jest does _not_ inject the global `jest` symbol, so Zone.js would not normally know it is running + * within Jest as users are supposed to import from `@jest/globals` or use `import.meta.jest`. Zone.js is not currently aware of this, so we + * manually set this global to get Zone.js to run correctly. + * + * TODO(dgp1130): Update Zone.js to directly support Jest ESM executions so we can drop this. + */ + +// eslint-disable-next-line no-undef +globalThis.jest = import.meta.jest; diff --git a/src/builders/jest/jest.config.mjs b/src/builders/jest/jest.config.mjs new file mode 100644 index 000000000..44b53d800 --- /dev/null +++ b/src/builders/jest/jest.config.mjs @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +// Empty config file, everything is specified via CLI options right now. +// This file is used just so Jest doesn't accidentally inherit a custom user-specified Jest config. +export default {}; diff --git a/src/builders/jest/options.d.ts b/src/builders/jest/options.d.ts new file mode 100644 index 000000000..5bf2872b6 --- /dev/null +++ b/src/builders/jest/options.d.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Schema as JestBuilderSchema } from './schema'; +/** + * Options supported for the Jest builder. The schema is an approximate + * representation of the options type, but this is a more precise version. + */ +export type JestBuilderOptions = JestBuilderSchema & { + include: string[]; + exclude: string[]; +}; +/** + * Normalizes input options validated by the schema to a more precise and useful + * options type in {@link JestBuilderOptions}. + */ +export declare function normalizeOptions(schema: JestBuilderSchema): JestBuilderOptions; diff --git a/src/builders/jest/options.js b/src/builders/jest/options.js new file mode 100644 index 000000000..96e4b1de7 --- /dev/null +++ b/src/builders/jest/options.js @@ -0,0 +1,24 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeOptions = normalizeOptions; +/** + * Normalizes input options validated by the schema to a more precise and useful + * options type in {@link JestBuilderOptions}. + */ +function normalizeOptions(schema) { + return { + // Options with default values can't actually be null, even if the types say so. + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + include: schema.include, + exclude: schema.exclude, + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + ...schema, + }; +} diff --git a/src/builders/jest/schema.d.ts b/src/builders/jest/schema.d.ts new file mode 100644 index 000000000..3f79ce1a0 --- /dev/null +++ b/src/builders/jest/schema.d.ts @@ -0,0 +1,26 @@ +/** + * Jest target options + */ +export type Schema = { + /** + * Run tests using Ahead of Time compilation. + */ + aot?: boolean; + /** + * Globs of files to exclude, relative to the project root. + */ + exclude?: string[]; + /** + * Globs of files to include, relative to project root. + */ + include?: string[]; + /** + * A list of polyfills to include in the build. Can be a full path for a file, relative to + * the current workspace or module specifier. Example: 'zone.js'. + */ + polyfills?: string[]; + /** + * The name of the TypeScript configuration file. + */ + tsConfig: string; +}; diff --git a/src/karma/schema.js b/src/builders/jest/schema.js similarity index 100% rename from src/karma/schema.js rename to src/builders/jest/schema.js diff --git a/src/builders/jest/schema.json b/src/builders/jest/schema.json new file mode 100644 index 000000000..272a1a906 --- /dev/null +++ b/src/builders/jest/schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Jest browser schema for Build Facade.", + "description": "Jest target options", + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + }, + "default": ["**/*.spec.ts"], + "description": "Globs of files to include, relative to project root." + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "Globs of files to exclude, relative to the project root." + }, + "tsConfig": { + "type": "string", + "description": "The name of the TypeScript configuration file." + }, + "polyfills": { + "type": "array", + "description": "A list of polyfills to include in the build. Can be a full path for a file, relative to the current workspace or module specifier. Example: 'zone.js'.", + "items": { + "type": "string", + "uniqueItems": true + }, + "default": [] + }, + "aot": { + "type": "boolean", + "description": "Run tests using Ahead of Time compilation.", + "default": false + } + }, + "additionalProperties": false, + "required": ["tsConfig"] +} diff --git a/src/builders/karma/browser_builder.d.ts b/src/builders/karma/browser_builder.d.ts new file mode 100644 index 000000000..41768bb46 --- /dev/null +++ b/src/builders/karma/browser_builder.d.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import type { ConfigOptions } from 'karma'; +import { Observable } from 'rxjs'; +import { Configuration } from 'webpack'; +import { ExecutionTransformer } from '../../transforms'; +import { Schema as KarmaBuilderOptions } from './schema'; +export type KarmaConfigOptions = ConfigOptions & { + buildWebpack?: unknown; + configFile?: string; +}; +export declare function execute(options: KarmaBuilderOptions, context: BuilderContext, karmaOptions: KarmaConfigOptions, transforms?: { + webpackConfiguration?: ExecutionTransformer; + karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions; +}): Observable; diff --git a/src/builders/karma/browser_builder.js b/src/builders/karma/browser_builder.js new file mode 100644 index 000000000..03014a399 --- /dev/null +++ b/src/builders/karma/browser_builder.js @@ -0,0 +1,148 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = execute; +const private_1 = require("@angular/build/private"); +const path = __importStar(require("node:path")); +const rxjs_1 = require("rxjs"); +const configs_1 = require("../../tools/webpack/configs"); +const webpack_browser_config_1 = require("../../utils/webpack-browser-config"); +const schema_1 = require("../browser/schema"); +const find_tests_plugin_1 = require("./find-tests-plugin"); +function execute(options, context, karmaOptions, transforms = {}) { + return (0, rxjs_1.from)(initializeBrowser(options, context, transforms.webpackConfiguration)).pipe((0, rxjs_1.switchMap)(async ([karma, webpackConfig]) => { + const projectName = context.target?.project; + if (!projectName) { + throw new Error(`The 'karma' builder requires a target to be specified.`); + } + const projectMetadata = await context.getProjectMetadata(projectName); + const sourceRoot = (projectMetadata.sourceRoot ?? projectMetadata.root ?? ''); + if (!options.main) { + webpackConfig.entry ??= {}; + if (typeof webpackConfig.entry === 'object' && !Array.isArray(webpackConfig.entry)) { + if (Array.isArray(webpackConfig.entry['main'])) { + webpackConfig.entry['main'].push(getBuiltInMainFile()); + } + else { + webpackConfig.entry['main'] = [getBuiltInMainFile()]; + } + } + } + webpackConfig.plugins ??= []; + webpackConfig.plugins.push(new find_tests_plugin_1.FindTestsPlugin({ + include: options.include, + exclude: options.exclude, + workspaceRoot: context.workspaceRoot, + projectSourceRoot: path.join(context.workspaceRoot, sourceRoot), + })); + karmaOptions.buildWebpack = { + options, + webpackConfig, + logger: context.logger, + }; + const parsedKarmaConfig = await karma.config.parseConfig(options.karmaConfig && path.resolve(context.workspaceRoot, options.karmaConfig), transforms.karmaOptions ? transforms.karmaOptions(karmaOptions) : karmaOptions, { promiseConfig: true, throwErrors: true }); + return [karma, parsedKarmaConfig]; + }), (0, rxjs_1.switchMap)(([karma, karmaConfig]) => new rxjs_1.Observable((subscriber) => { + // Pass onto Karma to emit BuildEvents. + karmaConfig.buildWebpack ??= {}; + if (typeof karmaConfig.buildWebpack === 'object') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + karmaConfig.buildWebpack.failureCb ??= () => subscriber.next({ success: false }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + karmaConfig.buildWebpack.successCb ??= () => subscriber.next({ success: true }); + } + // Complete the observable once the Karma server returns. + const karmaServer = new karma.Server(karmaConfig, (exitCode) => { + subscriber.next({ success: exitCode === 0 }); + subscriber.complete(); + }); + const karmaStart = karmaServer.start(); + // Cleanup, signal Karma to exit. + return () => { + void karmaStart.then(() => karmaServer.stop()); + }; + })), (0, rxjs_1.defaultIfEmpty)({ success: false })); +} +async function initializeBrowser(options, context, webpackConfigurationTransformer) { + // Purge old build disk cache. + await (0, private_1.purgeStaleBuildCache)(context); + const karma = await Promise.resolve().then(() => __importStar(require('karma'))); + const { config } = await (0, webpack_browser_config_1.generateBrowserWebpackConfigFromContext)( + // only two properties are missing: + // * `outputPath` which is fixed for tests + // * `budgets` which might be incorrect due to extra dev libs + { + ...options, + outputPath: '', + budgets: undefined, + optimization: false, + buildOptimizer: false, + aot: options.aot, + vendorChunk: true, + namedChunks: true, + extractLicenses: false, + outputHashing: schema_1.OutputHashing.None, + // The webpack tier owns the watch behavior so we want to force it in the config. + // When not in watch mode, webpack-dev-middleware will call `compiler.watch` anyway. + // https://github.com/webpack/webpack-dev-middleware/blob/698c9ae5e9bb9a013985add6189ff21c1a1ec185/src/index.js#L65 + // https://github.com/webpack/webpack/blob/cde1b73e12eb8a77eb9ba42e7920c9ec5d29c2c9/lib/Compiler.js#L379-L388 + watch: true, + }, context, (wco) => [(0, configs_1.getCommonConfig)(wco), (0, configs_1.getStylesConfig)(wco)]); + return [karma, (await webpackConfigurationTransformer?.(config)) ?? config]; +} +function getBuiltInMainFile(includeZoneProvider = false) { + const content = Buffer.from(` + import { provideZoneChangeDetection, ɵcompileNgModuleDefs as compileNgModuleDefs } from '@angular/core'; + import { getTestBed } from '@angular/core/testing'; + import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; + + export class TestModule {} + compileNgModuleDefs(TestModule, {providers: [${includeZoneProvider ? 'provideZoneChangeDetection()' : ''}]}); + + // Initialize the Angular testing environment. + getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), { + errorOnUnknownElements: true, + errorOnUnknownProperties: true + }); +`).toString('base64'); + return `ng-virtual-main.js!=!data:text/javascript;base64,${content}`; +} diff --git a/src/builders/karma/find-tests-plugin.d.ts b/src/builders/karma/find-tests-plugin.d.ts new file mode 100644 index 000000000..ada254f69 --- /dev/null +++ b/src/builders/karma/find-tests-plugin.d.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { Compiler } from 'webpack'; +export interface FindTestsPluginOptions { + include?: string[]; + exclude?: string[]; + workspaceRoot: string; + projectSourceRoot: string; +} +export declare class FindTestsPlugin { + private options; + private compilation; + constructor(options: FindTestsPluginOptions); + apply(compiler: Compiler): void; +} diff --git a/src/builders/karma/find-tests-plugin.js b/src/builders/karma/find-tests-plugin.js new file mode 100644 index 000000000..53a82af73 --- /dev/null +++ b/src/builders/karma/find-tests-plugin.js @@ -0,0 +1,58 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FindTestsPlugin = void 0; +const private_1 = require("@angular/build/private"); +const mini_css_extract_plugin_1 = require("mini-css-extract-plugin"); +const node_assert_1 = __importDefault(require("node:assert")); +/** + * The name of the plugin provided to Webpack when tapping Webpack compiler hooks. + */ +const PLUGIN_NAME = 'angular-find-tests-plugin'; +class FindTestsPlugin { + options; + compilation; + constructor(options) { + this.options = options; + } + apply(compiler) { + const { include = ['**/*.spec.ts'], exclude = [], projectSourceRoot, workspaceRoot, } = this.options; + const webpackOptions = compiler.options; + const entry = typeof webpackOptions.entry === 'function' ? webpackOptions.entry() : webpackOptions.entry; + let originalImport; + // Add tests files are part of the entry-point. + webpackOptions.entry = async () => { + const specFiles = await (0, private_1.findTests)(include, exclude, workspaceRoot, projectSourceRoot); + const entrypoints = await entry; + const entrypoint = entrypoints['main']; + if (!entrypoint.import) { + throw new Error(`Cannot find 'main' entrypoint.`); + } + if (specFiles.length) { + originalImport ??= entrypoint.import; + entrypoint.import = [...originalImport, ...specFiles]; + } + else { + (0, node_assert_1.default)(this.compilation, 'Compilation cannot be undefined.'); + this.compilation + .getLogger(mini_css_extract_plugin_1.pluginName) + .error(`Specified patterns: "${include.join(', ')}" did not match any spec files.`); + } + return entrypoints; + }; + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { + this.compilation = compilation; + compilation.contextDependencies.add(projectSourceRoot); + }); + } +} +exports.FindTestsPlugin = FindTestsPlugin; diff --git a/src/builders/karma/index.d.ts b/src/builders/karma/index.d.ts new file mode 100644 index 000000000..e260a86e2 --- /dev/null +++ b/src/builders/karma/index.d.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import type { ConfigOptions } from 'karma'; +import { Observable } from 'rxjs'; +import { Configuration } from 'webpack'; +import { ExecutionTransformer } from '../../transforms'; +import { Schema as KarmaBuilderOptions } from './schema'; +export type KarmaConfigOptions = ConfigOptions & { + buildWebpack?: unknown; + configFile?: string; +}; +/** + * @experimental Direct usage of this function is considered experimental. + */ +export declare function execute(options: KarmaBuilderOptions, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; + karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions; +}): Observable; +export type { KarmaBuilderOptions }; +declare const _default: import("@angular-devkit/architect").Builder & KarmaBuilderOptions & import("@angular-devkit/core").JsonObject>; +export default _default; diff --git a/src/builders/karma/index.js b/src/builders/karma/index.js new file mode 100644 index 000000000..c3dcec061 --- /dev/null +++ b/src/builders/karma/index.js @@ -0,0 +1,198 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = execute; +const private_1 = require("@angular/build/private"); +const architect_1 = require("@angular-devkit/architect"); +const core_1 = require("@angular-devkit/core"); +const node_module_1 = require("node:module"); +const path = __importStar(require("node:path")); +const rxjs_1 = require("rxjs"); +const utils_1 = require("../../utils"); +const schema_1 = require("./schema"); +/** + * @experimental Direct usage of this function is considered experimental. + */ +function execute(options, context, transforms = {}) { + // Check Angular version. + (0, private_1.assertCompatibleAngularVersion)(context.workspaceRoot); + return (0, rxjs_1.from)(getExecuteWithBuilder(options, context)).pipe((0, rxjs_1.mergeMap)(([useEsbuild, executeWithBuilder]) => { + if (useEsbuild) { + if (transforms.webpackConfiguration) { + context.logger.warn(`This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.`); + } + if (options.fileReplacements) { + options.fileReplacements = (0, utils_1.normalizeFileReplacements)(options.fileReplacements, './'); + } + if (typeof options.polyfills === 'string') { + options.polyfills = [options.polyfills]; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return executeWithBuilder(options, context, transforms); + } + else { + const karmaOptions = getBaseKarmaOptions(options, context); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return executeWithBuilder(options, context, karmaOptions, transforms); + } + })); +} +function getBaseKarmaOptions(options, context) { + let singleRun; + if (options.watch !== undefined) { + singleRun = !options.watch; + } + // Determine project name from builder context target + const projectName = context.target?.project; + if (!projectName) { + throw new Error(`The 'karma' builder requires a target to be specified.`); + } + const karmaOptions = options.karmaConfig + ? {} + : getBuiltInKarmaConfig(context.workspaceRoot, projectName); + karmaOptions.singleRun = singleRun; + // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default + // for single run executions. Not clearing context for multi-run (watched) builds allows the + // Jasmine Spec Runner to be visible in the browser after test execution. + karmaOptions.client ??= {}; + karmaOptions.client.clearContext ??= singleRun ?? false; // `singleRun` defaults to `false` per Karma docs. + // Convert browsers from a string to an array + if (typeof options.browsers === 'string' && options.browsers) { + karmaOptions.browsers = options.browsers.split(','); + } + else if (options.browsers === false) { + karmaOptions.browsers = []; + } + if (options.reporters) { + // Split along commas to make it more natural, and remove empty strings. + const reporters = options.reporters + .reduce((acc, curr) => acc.concat(curr.split(',')), []) + .filter((x) => !!x); + if (reporters.length > 0) { + karmaOptions.reporters = reporters; + } + } + return karmaOptions; +} +function getBuiltInKarmaConfig(workspaceRoot, projectName) { + let coverageFolderName = projectName.charAt(0) === '@' ? projectName.slice(1) : projectName; + if (/[A-Z]/.test(coverageFolderName)) { + coverageFolderName = core_1.strings.dasherize(coverageFolderName); + } + const workspaceRootRequire = (0, node_module_1.createRequire)(workspaceRoot + '/'); + // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template + return { + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + 'karma-jasmine', + 'karma-chrome-launcher', + 'karma-jasmine-html-reporter', + 'karma-coverage', + '@angular-devkit/build-angular/plugins/karma', + ].map((p) => workspaceRootRequire(p)), + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + coverageReporter: { + dir: path.join(workspaceRoot, 'coverage', coverageFolderName), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }], + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + customLaunchers: { + // Chrome configured to run in a bazel sandbox. + // Disable the use of the gpu and `/dev/shm` because it causes Chrome to + // crash on some environments. + // See: + // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips + // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'], + }, + }, + restartOnFileChange: true, + }; +} +exports.default = (0, architect_1.createBuilder)(execute); +async function getExecuteWithBuilder(options, context) { + const useEsbuild = await checkForEsbuild(options, context); + let execute; + if (useEsbuild) { + const { executeKarmaBuilder } = await Promise.resolve().then(() => __importStar(require('@angular/build'))); + execute = executeKarmaBuilder; + } + else { + const browserBuilderModule = await Promise.resolve().then(() => __importStar(require('./browser_builder'))); + execute = browserBuilderModule.execute; + } + return [useEsbuild, execute]; +} +async function checkForEsbuild(options, context) { + if (options.builderMode !== schema_1.BuilderMode.Detect) { + return options.builderMode === schema_1.BuilderMode.Application; + } + // Look up the current project's build target using a development configuration. + const buildTargetSpecifier = `::development`; + const buildTarget = (0, architect_1.targetFromTargetString)(buildTargetSpecifier, context.target?.project, 'build'); + try { + const developmentBuilderName = await context.getBuilderNameForTarget(buildTarget); + return isEsbuildBased(developmentBuilderName); + } + catch (e) { + if (!(e instanceof Error) || e.message !== 'Project target does not exist.') { + throw e; + } + // If we can't find a development builder, we can't use 'detect'. + throw new Error('Failed to detect the builder used by the application. Please set builderMode explicitly.'); + } +} +function isEsbuildBased(builderName) { + if (builderName === '@angular/build:application' || + builderName === '@angular-devkit/build-angular:application' || + builderName === '@angular-devkit/build-angular:browser-esbuild') { + return true; + } + return false; +} diff --git a/src/builders/karma/schema.d.ts b/src/builders/karma/schema.d.ts new file mode 100644 index 000000000..b91d38e90 --- /dev/null +++ b/src/builders/karma/schema.d.ts @@ -0,0 +1,218 @@ +/** + * Karma target options for Build Facade. + */ +export type Schema = { + /** + * Run tests using Ahead of Time compilation. + */ + aot?: boolean; + /** + * List of static application assets. + */ + assets?: AssetPattern[]; + /** + * Override which browsers tests are run against. Set to `false` to not use any browser. + */ + browsers?: Browsers; + /** + * Determines how to build the code under test. If set to 'detect', attempts to follow the + * development builder. + */ + builderMode?: BuilderMode; + /** + * Output a code coverage report. + */ + codeCoverage?: boolean; + /** + * Globs to exclude from code coverage. + */ + codeCoverageExclude?: string[]; + /** + * Globs of files to exclude, relative to the project root. + */ + exclude?: string[]; + /** + * Replace compilation source files with other compilation source files in the build. + */ + fileReplacements?: FileReplacement[]; + /** + * Globs of files to include, relative to project root. + * There are 2 special cases: + * - when a path to directory is provided, all spec files ending ".spec.@(ts|tsx)" will be + * included + * - when a path to a file is provided, and a matching spec file exists it will be included + * instead. + */ + include?: string[]; + /** + * The stylesheet language to use for the application's inline component styles. + */ + inlineStyleLanguage?: InlineStyleLanguage; + /** + * The name of the Karma configuration file. + */ + karmaConfig?: string; + /** + * The name of the main entry-point file. + */ + main?: string; + /** + * Enable and define the file watching poll time period in milliseconds. + */ + poll?: number; + /** + * Polyfills to be included in the build. + */ + polyfills?: Polyfills; + /** + * Do not use the real path when resolving modules. If unset then will default to `true` if + * NodeJS option --preserve-symlinks is set. + */ + preserveSymlinks?: boolean; + /** + * Log progress to the console while building. + */ + progress?: boolean; + /** + * Karma reporters to use. Directly passed to the karma runner. + */ + reporters?: string[]; + /** + * Global scripts to be included in the build. + */ + scripts?: ScriptElement[]; + /** + * Output source maps for scripts and styles. For more information, see + * https://angular.dev/reference/configs/workspace-config#source-map-configuration. + */ + sourceMap?: SourceMapUnion; + /** + * Options to pass to style preprocessors + */ + stylePreprocessorOptions?: StylePreprocessorOptions; + /** + * Global styles to be included in the build. + */ + styles?: StyleElement[]; + /** + * The name of the TypeScript configuration file. + */ + tsConfig: string; + /** + * Run build when files change. + */ + watch?: boolean; + /** + * TypeScript configuration for Web Worker modules. + */ + webWorkerTsConfig?: string; +}; +export type AssetPattern = AssetPatternClass | string; +export type AssetPatternClass = { + /** + * The pattern to match. + */ + glob: string; + /** + * An array of globs to ignore. + */ + ignore?: string[]; + /** + * The input directory path in which to apply 'glob'. Defaults to the project root. + */ + input: string; + /** + * Absolute path within the output. + */ + output?: string; +}; +/** + * Override which browsers tests are run against. Set to `false` to not use any browser. + */ +export type Browsers = boolean | string; +/** + * Determines how to build the code under test. If set to 'detect', attempts to follow the + * development builder. + */ +export declare enum BuilderMode { + Application = "application", + Browser = "browser", + Detect = "detect" +} +export type FileReplacement = { + replace?: string; + replaceWith?: string; + src?: string; + with?: string; +}; +/** + * The stylesheet language to use for the application's inline component styles. + */ +export declare enum InlineStyleLanguage { + Css = "css", + Less = "less", + Sass = "sass", + Scss = "scss" +} +/** + * Polyfills to be included in the build. + */ +export type Polyfills = string[] | string; +export type ScriptElement = ScriptClass | string; +export type ScriptClass = { + /** + * The bundle name for this extra entry point. + */ + bundleName?: string; + /** + * If the bundle will be referenced in the HTML file. + */ + inject?: boolean; + /** + * The file to include. + */ + input: string; +}; +/** + * Output source maps for scripts and styles. For more information, see + * https://angular.dev/reference/configs/workspace-config#source-map-configuration. + */ +export type SourceMapUnion = boolean | SourceMapClass; +export type SourceMapClass = { + /** + * Output source maps for all scripts. + */ + scripts?: boolean; + /** + * Output source maps for all styles. + */ + styles?: boolean; + /** + * Resolve vendor packages source maps. + */ + vendor?: boolean; +}; +/** + * Options to pass to style preprocessors + */ +export type StylePreprocessorOptions = { + /** + * Paths to include. Paths will be resolved to workspace root. + */ + includePaths?: string[]; +}; +export type StyleElement = StyleClass | string; +export type StyleClass = { + /** + * The bundle name for this extra entry point. + */ + bundleName?: string; + /** + * If the bundle will be referenced in the HTML file. + */ + inject?: boolean; + /** + * The file to include. + */ + input: string; +}; diff --git a/src/builders/karma/schema.js b/src/builders/karma/schema.js new file mode 100644 index 000000000..a46e4a639 --- /dev/null +++ b/src/builders/karma/schema.js @@ -0,0 +1,25 @@ +"use strict"; +// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE +// CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). +Object.defineProperty(exports, "__esModule", { value: true }); +exports.InlineStyleLanguage = exports.BuilderMode = void 0; +/** + * Determines how to build the code under test. If set to 'detect', attempts to follow the + * development builder. + */ +var BuilderMode; +(function (BuilderMode) { + BuilderMode["Application"] = "application"; + BuilderMode["Browser"] = "browser"; + BuilderMode["Detect"] = "detect"; +})(BuilderMode || (exports.BuilderMode = BuilderMode = {})); +/** + * The stylesheet language to use for the application's inline component styles. + */ +var InlineStyleLanguage; +(function (InlineStyleLanguage) { + InlineStyleLanguage["Css"] = "css"; + InlineStyleLanguage["Less"] = "less"; + InlineStyleLanguage["Sass"] = "sass"; + InlineStyleLanguage["Scss"] = "scss"; +})(InlineStyleLanguage || (exports.InlineStyleLanguage = InlineStyleLanguage = {})); diff --git a/src/builders/karma/schema.json b/src/builders/karma/schema.json new file mode 100644 index 000000000..c8ed717a9 --- /dev/null +++ b/src/builders/karma/schema.json @@ -0,0 +1,324 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Karma Target", + "description": "Karma target options for Build Facade.", + "type": "object", + "properties": { + "main": { + "type": "string", + "description": "The name of the main entry-point file." + }, + "tsConfig": { + "type": "string", + "description": "The name of the TypeScript configuration file." + }, + "karmaConfig": { + "type": "string", + "description": "The name of the Karma configuration file." + }, + "polyfills": { + "description": "Polyfills to be included in the build.", + "oneOf": [ + { + "type": "array", + "description": "A list of polyfills to include in the build. Can be a full path for a file, relative to the current workspace or module specifier. Example: 'zone.js'.", + "items": { + "type": "string", + "uniqueItems": true + }, + "default": [] + }, + { + "type": "string", + "description": "The full path for the polyfills file, relative to the current workspace or a module specifier. Example: 'zone.js'." + } + ] + }, + "assets": { + "type": "array", + "description": "List of static application assets.", + "default": [], + "items": { + "$ref": "#/definitions/assetPattern" + } + }, + "scripts": { + "description": "Global scripts to be included in the build.", + "type": "array", + "default": [], + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include.", + "pattern": "\\.[cm]?jsx?$" + }, + "bundleName": { + "type": "string", + "pattern": "^[\\w\\-.]*$", + "description": "The bundle name for this extra entry point." + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true + } + }, + "additionalProperties": false, + "required": ["input"] + }, + { + "type": "string", + "description": "The file to include.", + "pattern": "\\.[cm]?jsx?$" + } + ] + } + }, + "styles": { + "description": "Global styles to be included in the build.", + "type": "array", + "default": [], + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include.", + "pattern": "\\.(?:css|scss|sass|less)$" + }, + "bundleName": { + "type": "string", + "pattern": "^[\\w\\-.]*$", + "description": "The bundle name for this extra entry point." + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true + } + }, + "additionalProperties": false, + "required": ["input"] + }, + { + "type": "string", + "description": "The file to include.", + "pattern": "\\.(?:css|scss|sass|less)$" + } + ] + } + }, + "inlineStyleLanguage": { + "description": "The stylesheet language to use for the application's inline component styles.", + "type": "string", + "default": "css", + "enum": ["css", "less", "sass", "scss"] + }, + "stylePreprocessorOptions": { + "description": "Options to pass to style preprocessors", + "type": "object", + "properties": { + "includePaths": { + "description": "Paths to include. Paths will be resolved to workspace root.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + } + }, + "additionalProperties": false + }, + "include": { + "type": "array", + "items": { + "type": "string" + }, + "default": ["**/*.spec.ts"], + "description": "Globs of files to include, relative to project root. \nThere are 2 special cases:\n - when a path to directory is provided, all spec files ending \".spec.@(ts|tsx)\" will be included\n - when a path to a file is provided, and a matching spec file exists it will be included instead." + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "Globs of files to exclude, relative to the project root." + }, + "sourceMap": { + "description": "Output source maps for scripts and styles. For more information, see https://angular.dev/reference/configs/workspace-config#source-map-configuration.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output source maps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output source maps for all styles.", + "default": true + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages source maps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + }, + "progress": { + "type": "boolean", + "description": "Log progress to the console while building.", + "default": true + }, + "watch": { + "type": "boolean", + "description": "Run build when files change." + }, + "poll": { + "type": "number", + "description": "Enable and define the file watching poll time period in milliseconds." + }, + "preserveSymlinks": { + "type": "boolean", + "description": "Do not use the real path when resolving modules. If unset then will default to `true` if NodeJS option --preserve-symlinks is set." + }, + "browsers": { + "description": "Override which browsers tests are run against. Set to `false` to not use any browser.", + "oneOf": [ + { + "type": "string", + "description": "A comma seperate list of browsers to run tests against." + }, + { + "const": false, + "type": "boolean", + "description": "Does use run tests against a browser." + } + ] + }, + "codeCoverage": { + "type": "boolean", + "description": "Output a code coverage report.", + "default": false + }, + "codeCoverageExclude": { + "type": "array", + "description": "Globs to exclude from code coverage.", + "items": { + "type": "string" + }, + "default": [] + }, + "fileReplacements": { + "description": "Replace compilation source files with other compilation source files in the build.", + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "replaceWith": { + "type": "string" + } + }, + "additionalProperties": false, + "required": ["src", "replaceWith"] + }, + { + "type": "object", + "properties": { + "replace": { + "type": "string" + }, + "with": { + "type": "string" + } + }, + "additionalProperties": false, + "required": ["replace", "with"] + } + ] + }, + "default": [] + }, + "reporters": { + "type": "array", + "description": "Karma reporters to use. Directly passed to the karma runner.", + "items": { + "type": "string" + } + }, + "builderMode": { + "type": "string", + "description": "Determines how to build the code under test. If set to 'detect', attempts to follow the development builder.", + "enum": ["detect", "browser", "application"], + "default": "browser" + }, + "webWorkerTsConfig": { + "type": "string", + "description": "TypeScript configuration for Web Worker modules." + }, + "aot": { + "type": "boolean", + "description": "Run tests using Ahead of Time compilation.", + "default": false + } + }, + "additionalProperties": false, + "required": ["tsConfig"], + "definitions": { + "assetPattern": { + "oneOf": [ + { + "type": "object", + "properties": { + "glob": { + "type": "string", + "description": "The pattern to match." + }, + "input": { + "type": "string", + "description": "The input directory path in which to apply 'glob'. Defaults to the project root." + }, + "output": { + "type": "string", + "default": "", + "description": "Absolute path within the output." + }, + "ignore": { + "description": "An array of globs to ignore.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": ["glob", "input"] + }, + { + "type": "string" + } + ] + } + } +} diff --git a/src/builders/ng-packagr/index.d.ts b/src/builders/ng-packagr/index.d.ts new file mode 100644 index 000000000..818139ff0 --- /dev/null +++ b/src/builders/ng-packagr/index.d.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import { Observable } from 'rxjs'; +import { Schema as NgPackagrBuilderOptions } from './schema'; +/** + * @experimental Direct usage of this function is considered experimental. + */ +export declare function execute(options: NgPackagrBuilderOptions, context: BuilderContext): Observable; +export type { NgPackagrBuilderOptions }; +declare const _default: import("@angular-devkit/architect").Builder & NgPackagrBuilderOptions & import("@angular-devkit/core").JsonObject>; +export default _default; diff --git a/src/builders/ng-packagr/index.js b/src/builders/ng-packagr/index.js new file mode 100644 index 000000000..2a27fd490 --- /dev/null +++ b/src/builders/ng-packagr/index.js @@ -0,0 +1,76 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = execute; +const private_1 = require("@angular/build/private"); +const architect_1 = require("@angular-devkit/architect"); +const node_path_1 = require("node:path"); +const rxjs_1 = require("rxjs"); +const normalize_cache_1 = require("../../utils/normalize-cache"); +/** + * @experimental Direct usage of this function is considered experimental. + */ +function execute(options, context) { + return (0, rxjs_1.from)((async () => { + // Purge old build disk cache. + await (0, private_1.purgeStaleBuildCache)(context); + const root = context.workspaceRoot; + const packager = (await Promise.resolve().then(() => __importStar(require('ng-packagr')))).ngPackagr(); + packager.forProject((0, node_path_1.resolve)(root, options.project)); + if (options.tsConfig) { + packager.withTsConfig((0, node_path_1.resolve)(root, options.tsConfig)); + } + const projectName = context.target?.project; + if (!projectName) { + throw new Error('The builder requires a target.'); + } + const metadata = await context.getProjectMetadata(projectName); + const { enabled: cacheEnabled, path: cacheDirectory } = (0, normalize_cache_1.normalizeCacheOptions)(metadata, context.workspaceRoot); + const ngPackagrOptions = { + cacheEnabled, + poll: options.poll, + cacheDirectory: (0, node_path_1.join)(cacheDirectory, 'ng-packagr'), + }; + return { packager, ngPackagrOptions }; + })()).pipe((0, rxjs_1.switchMap)(({ packager, ngPackagrOptions }) => options.watch ? packager.watch(ngPackagrOptions) : packager.build(ngPackagrOptions)), (0, rxjs_1.map)(() => ({ success: true })), (0, rxjs_1.catchError)((err) => (0, rxjs_1.of)({ success: false, error: err.message }))); +} +exports.default = (0, architect_1.createBuilder)(execute); diff --git a/src/builders/ng-packagr/schema.d.ts b/src/builders/ng-packagr/schema.d.ts new file mode 100644 index 000000000..5607385cf --- /dev/null +++ b/src/builders/ng-packagr/schema.d.ts @@ -0,0 +1,21 @@ +/** + * ng-packagr target options for Build Architect. Use to build library projects. + */ +export type Schema = { + /** + * Enable and define the file watching poll time period in milliseconds. + */ + poll?: number; + /** + * The file path for the ng-packagr configuration file, relative to the current workspace. + */ + project: string; + /** + * The full path for the TypeScript configuration file, relative to the current workspace. + */ + tsConfig?: string; + /** + * Run build when files change. + */ + watch?: boolean; +}; diff --git a/src/protractor/schema.js b/src/builders/ng-packagr/schema.js similarity index 100% rename from src/protractor/schema.js rename to src/builders/ng-packagr/schema.js diff --git a/src/builders/ng-packagr/schema.json b/src/builders/ng-packagr/schema.json new file mode 100644 index 000000000..da76255f0 --- /dev/null +++ b/src/builders/ng-packagr/schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "ng-packagr Target", + "description": "ng-packagr target options for Build Architect. Use to build library projects.", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The file path for the ng-packagr configuration file, relative to the current workspace." + }, + "tsConfig": { + "type": "string", + "description": "The full path for the TypeScript configuration file, relative to the current workspace." + }, + "watch": { + "type": "boolean", + "description": "Run build when files change.", + "default": false + }, + "poll": { + "type": "number", + "description": "Enable and define the file watching poll time period in milliseconds." + } + }, + "additionalProperties": false, + "required": ["project"] +} diff --git a/src/builders/prerender/index.d.ts b/src/builders/prerender/index.d.ts new file mode 100644 index 000000000..469af6fcb --- /dev/null +++ b/src/builders/prerender/index.d.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import { Schema } from './schema'; +type PrerenderBuilderOptions = Schema; +type PrerenderBuilderOutput = BuilderOutput; +/** + * Builds the browser and server, then renders each route in options.routes + * and writes them to prerender//index.html for each output path in + * the browser result. + */ +export declare function execute(options: PrerenderBuilderOptions, context: BuilderContext): Promise; +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; diff --git a/src/builders/prerender/index.js b/src/builders/prerender/index.js new file mode 100644 index 000000000..f459eb96a --- /dev/null +++ b/src/builders/prerender/index.js @@ -0,0 +1,241 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = execute; +const private_1 = require("@angular/build/private"); +const architect_1 = require("@angular-devkit/architect"); +const fs = __importStar(require("node:fs")); +const promises_1 = require("node:fs/promises"); +const path = __importStar(require("node:path")); +const ora_1 = __importDefault(require("ora")); +const piscina_1 = __importDefault(require("piscina")); +const utils_1 = require("../../utils"); +const environment_options_1 = require("../../utils/environment-options"); +const error_1 = require("../../utils/error"); +const webpack_browser_config_1 = require("../../utils/webpack-browser-config"); +class RoutesSet extends Set { + add(value) { + return super.add(value.charAt(0) === '/' ? value.slice(1) : value); + } +} +async function getRoutes(indexFile, outputPath, serverBundlePath, options, workspaceRoot) { + const { routes: extraRoutes = [], routesFile, discoverRoutes } = options; + const routes = new RoutesSet(extraRoutes); + if (routesFile) { + const routesFromFile = (await (0, promises_1.readFile)(path.join(workspaceRoot, routesFile), 'utf8')).split(/\r?\n/); + for (const route of routesFromFile) { + routes.add(route); + } + } + let zonePackage; + try { + zonePackage = require.resolve('zone.js/node', { paths: [workspaceRoot] }); + } + catch { } + if (discoverRoutes) { + const renderWorker = new piscina_1.default({ + filename: require.resolve('./routes-extractor-worker'), + maxThreads: 1, + workerData: { + indexFile, + outputPath, + serverBundlePath, + zonePackage, + }, + recordTiming: false, + }); + const extractedRoutes = await renderWorker + .run({}) + .finally(() => void renderWorker.destroy()); + for (const route of extractedRoutes) { + routes.add(route); + } + } + if (routes.size === 0) { + throw new Error('Could not find any routes to prerender.'); + } + return [...routes]; +} +/** + * Schedules the server and browser builds and returns their results if both builds are successful. + */ +async function _scheduleBuilds(options, context) { + const browserTarget = (0, architect_1.targetFromTargetString)(options.browserTarget); + const serverTarget = (0, architect_1.targetFromTargetString)(options.serverTarget); + const browserTargetRun = await context.scheduleTarget(browserTarget, { + watch: false, + serviceWorker: false, + // todo: handle service worker augmentation + }); + if (browserTargetRun.info.builderName === '@angular-devkit/build-angular:application') { + return { + success: false, + error: '"@angular-devkit/build-angular:application" has built-in prerendering capabilities. ' + + 'The "prerender" option should be used instead.', + }; + } + const serverTargetRun = await context.scheduleTarget(serverTarget, { + watch: false, + }); + try { + const [browserResult, serverResult] = await Promise.all([ + browserTargetRun.result, + serverTargetRun.result, + ]); + const success = browserResult.success && serverResult.success && browserResult.baseOutputPath !== undefined; + const error = browserResult.error || serverResult.error; + return { success, error, browserResult, serverResult }; + } + catch (e) { + (0, error_1.assertIsError)(e); + return { success: false, error: e.message }; + } + finally { + await Promise.all([browserTargetRun.stop(), serverTargetRun.stop()]); + } +} +/** + * Renders each route and writes them to + * /index.html for each output path in the browser result. + */ +async function _renderUniversal(options, context, browserResult, serverResult, browserOptions) { + const projectName = context.target && context.target.project; + if (!projectName) { + throw new Error('The builder requires a target.'); + } + const projectMetadata = await context.getProjectMetadata(projectName); + const projectRoot = path.join(context.workspaceRoot, projectMetadata.root ?? ''); + // Users can specify a different base html file e.g. "src/home.html" + const indexFile = (0, webpack_browser_config_1.getIndexOutputFile)(browserOptions.index); + const { styles: normalizedStylesOptimization } = (0, utils_1.normalizeOptimization)(browserOptions.optimization); + let zonePackage; + try { + zonePackage = require.resolve('zone.js/node', { paths: [context.workspaceRoot] }); + } + catch { } + const { baseOutputPath = '' } = serverResult; + const worker = new piscina_1.default({ + filename: path.join(__dirname, 'render-worker.js'), + maxThreads: environment_options_1.maxWorkers, + workerData: { zonePackage }, + recordTiming: false, + }); + let routes; + try { + // We need to render the routes for each locale from the browser output. + for (const { path: outputPath } of browserResult.outputs) { + const localeDirectory = path.relative(browserResult.baseOutputPath, outputPath); + const serverBundlePath = path.join(baseOutputPath, localeDirectory, 'main.js'); + if (!fs.existsSync(serverBundlePath)) { + throw new Error(`Could not find the main bundle: ${serverBundlePath}`); + } + routes ??= await getRoutes(indexFile, outputPath, serverBundlePath, options, context.workspaceRoot); + const spinner = (0, ora_1.default)(`Prerendering ${routes.length} route(s) to ${outputPath}...`).start(); + try { + const results = (await Promise.all(routes.map((route) => { + const options = { + indexFile, + deployUrl: browserOptions.deployUrl || '', + inlineCriticalCss: !!normalizedStylesOptimization.inlineCritical, + minifyCss: !!normalizedStylesOptimization.minify, + outputPath, + route, + serverBundlePath, + }; + return worker.run(options); + }))); + let numErrors = 0; + for (const { errors, warnings } of results) { + spinner.stop(); + errors?.forEach((e) => context.logger.error(e)); + warnings?.forEach((e) => context.logger.warn(e)); + spinner.start(); + numErrors += errors?.length ?? 0; + } + if (numErrors > 0) { + throw Error(`Rendering failed with ${numErrors} worker errors.`); + } + } + catch (error) { + spinner.fail(`Prerendering routes to ${outputPath} failed.`); + (0, error_1.assertIsError)(error); + return { success: false, error: error.message }; + } + spinner.succeed(`Prerendering routes to ${outputPath} complete.`); + if (browserOptions.serviceWorker) { + spinner.start('Generating service worker...'); + try { + await (0, private_1.augmentAppWithServiceWorker)(projectRoot, context.workspaceRoot, outputPath, browserOptions.baseHref || '/', browserOptions.ngswConfigPath); + } + catch (error) { + spinner.fail('Service worker generation failed.'); + (0, error_1.assertIsError)(error); + return { success: false, error: error.message }; + } + spinner.succeed('Service worker generation complete.'); + } + } + } + finally { + void worker.destroy(); + } + return browserResult; +} +/** + * Builds the browser and server, then renders each route in options.routes + * and writes them to prerender//index.html for each output path in + * the browser result. + */ +async function execute(options, context) { + const browserTarget = (0, architect_1.targetFromTargetString)(options.browserTarget); + const browserOptions = (await context.getTargetOptions(browserTarget)); + const result = await _scheduleBuilds(options, context); + const { success, error, browserResult, serverResult } = result; + if (!success || !browserResult || !serverResult) { + return { success, error }; + } + return _renderUniversal(options, context, browserResult, serverResult, browserOptions); +} +exports.default = (0, architect_1.createBuilder)(execute); diff --git a/src/builders/prerender/render-worker.d.ts b/src/builders/prerender/render-worker.d.ts new file mode 100644 index 000000000..f5055ba96 --- /dev/null +++ b/src/builders/prerender/render-worker.d.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export interface RenderOptions { + indexFile: string; + deployUrl: string; + inlineCriticalCss: boolean; + minifyCss: boolean; + outputPath: string; + serverBundlePath: string; + route: string; +} +export interface RenderResult { + errors?: string[]; + warnings?: string[]; +} +/** + * Renders each route in routes and writes them to //index.html. + */ +declare function render({ indexFile, deployUrl, minifyCss, outputPath, serverBundlePath, route, inlineCriticalCss, }: RenderOptions): Promise; +/** + * The default export will be the promise returned by the initialize function. + * This is awaited by piscina prior to using the Worker. + */ +declare const _default: Promise; +export default _default; diff --git a/src/builders/prerender/render-worker.js b/src/builders/prerender/render-worker.js new file mode 100644 index 000000000..4e6939548 --- /dev/null +++ b/src/builders/prerender/render-worker.js @@ -0,0 +1,140 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_assert_1 = __importDefault(require("node:assert")); +const fs = __importStar(require("node:fs")); +const path = __importStar(require("node:path")); +const node_worker_threads_1 = require("node:worker_threads"); +/** + * The fully resolved path to the zone.js package that will be loaded during worker initialization. + * This is passed as workerData when setting up the worker via the `piscina` package. + */ +const { zonePackage } = node_worker_threads_1.workerData; +/** + * Renders each route in routes and writes them to //index.html. + */ +async function render({ indexFile, deployUrl, minifyCss, outputPath, serverBundlePath, route, inlineCriticalCss, }) { + const result = {}; + const browserIndexOutputPath = path.join(outputPath, indexFile); + const outputFolderPath = path.join(outputPath, route); + const outputIndexPath = path.join(outputFolderPath, 'index.html'); + const { ɵSERVER_CONTEXT, AppServerModule, renderModule, renderApplication, default: bootstrapAppFn, } = (await Promise.resolve(`${serverBundlePath}`).then(s => __importStar(require(s)))); + (0, node_assert_1.default)(ɵSERVER_CONTEXT, `ɵSERVER_CONTEXT was not exported from: ${serverBundlePath}.`); + const indexBaseName = fs.existsSync(path.join(outputPath, 'index.original.html')) + ? 'index.original.html' + : indexFile; + const browserIndexInputPath = path.join(outputPath, indexBaseName); + const document = await fs.promises.readFile(browserIndexInputPath, 'utf8'); + const platformProviders = [ + { + provide: ɵSERVER_CONTEXT, + useValue: 'ssg', + }, + ]; + let html; + // Render platform server module + if (isBootstrapFn(bootstrapAppFn)) { + (0, node_assert_1.default)(renderApplication, `renderApplication was not exported from: ${serverBundlePath}.`); + html = await renderApplication(bootstrapAppFn, { + document, + url: route, + platformProviders, + }); + } + else { + (0, node_assert_1.default)(renderModule, `renderModule was not exported from: ${serverBundlePath}.`); + const moduleClass = bootstrapAppFn || AppServerModule; + (0, node_assert_1.default)(moduleClass, `Neither an AppServerModule nor a bootstrapping function was exported from: ${serverBundlePath}.`); + html = await renderModule(moduleClass, { + document, + url: route, + extraProviders: platformProviders, + }); + } + if (inlineCriticalCss) { + const { InlineCriticalCssProcessor } = await Promise.resolve().then(() => __importStar(require('@angular/build/private'))); + const inlineCriticalCssProcessor = new InlineCriticalCssProcessor({ + deployUrl: deployUrl, + minify: minifyCss, + }); + const { content, warnings, errors } = await inlineCriticalCssProcessor.process(html, { + outputPath, + }); + result.errors = errors; + result.warnings = warnings; + html = content; + } + // This case happens when we are prerendering "/". + if (browserIndexOutputPath === outputIndexPath) { + const browserIndexOutputPathOriginal = path.join(outputPath, 'index.original.html'); + fs.renameSync(browserIndexOutputPath, browserIndexOutputPathOriginal); + } + fs.mkdirSync(outputFolderPath, { recursive: true }); + fs.writeFileSync(outputIndexPath, html); + return result; +} +function isBootstrapFn(value) { + // We can differentiate between a module and a bootstrap function by reading compiler-generated `ɵmod` static property: + return typeof value === 'function' && !('ɵmod' in value); +} +/** + * Initializes the worker when it is first created by loading the Zone.js package + * into the worker instance. + * + * @returns A promise resolving to the render function of the worker. + */ +async function initialize() { + if (zonePackage) { + // Setup Zone.js + await Promise.resolve(`${zonePackage}`).then(s => __importStar(require(s))); + } + // Return the render function for use + return render; +} +/** + * The default export will be the promise returned by the initialize function. + * This is awaited by piscina prior to using the Worker. + */ +exports.default = initialize(); diff --git a/src/builders/prerender/routes-extractor-worker.d.ts b/src/builders/prerender/routes-extractor-worker.d.ts new file mode 100644 index 000000000..2bd6aa0ad --- /dev/null +++ b/src/builders/prerender/routes-extractor-worker.d.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export interface RoutesExtractorWorkerData { + zonePackage: string | undefined; + indexFile: string; + outputPath: string; + serverBundlePath: string; +} +declare function extract(): Promise; +/** + * The default export will be the promise returned by the initialize function. + * This is awaited by piscina prior to using the Worker. + */ +declare const _default: Promise; +export default _default; diff --git a/src/builders/prerender/routes-extractor-worker.js b/src/builders/prerender/routes-extractor-worker.js new file mode 100644 index 000000000..d7b9a608b --- /dev/null +++ b/src/builders/prerender/routes-extractor-worker.js @@ -0,0 +1,83 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_assert_1 = __importDefault(require("node:assert")); +const fs = __importStar(require("node:fs")); +const path = __importStar(require("node:path")); +const node_worker_threads_1 = require("node:worker_threads"); +const { zonePackage, serverBundlePath, outputPath, indexFile } = node_worker_threads_1.workerData; +async function extract() { + const { AppServerModule, ɵgetRoutesFromAngularRouterConfig: getRoutesFromAngularRouterConfig, default: bootstrapAppFn, } = (await Promise.resolve(`${serverBundlePath}`).then(s => __importStar(require(s)))); + const browserIndexInputPath = path.join(outputPath, indexFile); + const document = await fs.promises.readFile(browserIndexInputPath, 'utf8'); + const bootstrapAppFnOrModule = bootstrapAppFn || AppServerModule; + (0, node_assert_1.default)(bootstrapAppFnOrModule, `The file "${serverBundlePath}" does not have a default export for an AppServerModule or a bootstrapping function.`); + const routes = []; + const { routes: extractRoutes } = await getRoutesFromAngularRouterConfig(bootstrapAppFnOrModule, document, new URL('http://localhost')); + for (const { route, redirectTo } of extractRoutes) { + if (redirectTo === undefined && !/[:*]/.test(route)) { + routes.push(route); + } + } + return routes; +} +/** + * Initializes the worker when it is first created by loading the Zone.js package + * into the worker instance. + * + * @returns A promise resolving to the extract function of the worker. + */ +async function initialize() { + if (zonePackage) { + // Setup Zone.js + await Promise.resolve(`${zonePackage}`).then(s => __importStar(require(s))); + } + return extract; +} +/** + * The default export will be the promise returned by the initialize function. + * This is awaited by piscina prior to using the Worker. + */ +exports.default = initialize(); diff --git a/src/builders/prerender/schema.d.ts b/src/builders/prerender/schema.d.ts new file mode 100644 index 000000000..907478ce5 --- /dev/null +++ b/src/builders/prerender/schema.d.ts @@ -0,0 +1,24 @@ +export type Schema = { + /** + * Target to build. + */ + browserTarget: string; + /** + * Whether the builder should process the Angular Router configuration to find all + * unparameterized routes and prerender them. + */ + discoverRoutes?: boolean; + /** + * The routes to render. + */ + routes?: string[]; + /** + * The path to a file that contains a list of all routes to prerender, separated by + * newlines. This option is useful if you want to prerender routes with parameterized URLs. + */ + routesFile?: string; + /** + * Server target to use for prerendering the app. + */ + serverTarget: string; +}; diff --git a/src/tslint/schema.js b/src/builders/prerender/schema.js similarity index 100% rename from src/tslint/schema.js rename to src/builders/prerender/schema.js diff --git a/src/builders/prerender/schema.json b/src/builders/prerender/schema.json new file mode 100644 index 000000000..18d7d8956 --- /dev/null +++ b/src/builders/prerender/schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Prerender Target", + "type": "object", + "properties": { + "browserTarget": { + "type": "string", + "description": "Target to build.", + "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" + }, + "serverTarget": { + "type": "string", + "description": "Server target to use for prerendering the app.", + "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" + }, + "routesFile": { + "type": "string", + "description": "The path to a file that contains a list of all routes to prerender, separated by newlines. This option is useful if you want to prerender routes with parameterized URLs." + }, + "routes": { + "type": "array", + "description": "The routes to render.", + "items": { + "minItems": 1, + "type": "string", + "uniqueItems": true + }, + "default": [] + }, + "discoverRoutes": { + "type": "boolean", + "description": "Whether the builder should process the Angular Router configuration to find all unparameterized routes and prerender them.", + "default": true + } + }, + "required": ["browserTarget", "serverTarget"], + "anyOf": [{ "required": ["routes"] }, { "required": ["routesFile"] }], + "additionalProperties": false +} diff --git a/src/builders/protractor-error/index.d.ts b/src/builders/protractor-error/index.d.ts new file mode 100644 index 000000000..aa72274c2 --- /dev/null +++ b/src/builders/protractor-error/index.d.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Schema as ProtractorBuilderOptions } from '../protractor/schema'; +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; diff --git a/src/builders/protractor-error/index.js b/src/builders/protractor-error/index.js new file mode 100644 index 000000000..0462a9e77 --- /dev/null +++ b/src/builders/protractor-error/index.js @@ -0,0 +1,14 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const architect_1 = require("@angular-devkit/architect"); +exports.default = (0, architect_1.createBuilder)((_options, context) => { + context.logger.error('Protractor has reached end-of-life and is no longer supported. For additional information and alternatives, please see https://blog.angular.dev/protractor-deprecation-update-august-2023-2beac7402ce0.'); + return { success: false }; +}); diff --git a/src/protractor/index.d.ts b/src/builders/protractor/index.d.ts similarity index 50% rename from src/protractor/index.d.ts rename to src/builders/protractor/index.d.ts index 321fd9224..fed087e1e 100644 --- a/src/protractor/index.d.ts +++ b/src/builders/protractor/index.d.ts @@ -1,14 +1,16 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; -import { JsonObject } from '@angular-devkit/core'; import { Schema as ProtractorBuilderOptions } from './schema'; -export { ProtractorBuilderOptions }; +export type { ProtractorBuilderOptions }; +/** + * @experimental Direct usage of this function is considered experimental. + */ export declare function execute(options: ProtractorBuilderOptions, context: BuilderContext): Promise; -declare const _default: import("@angular-devkit/architect/src/internal").Builder; +declare const _default: import("@angular-devkit/architect").Builder; export default _default; diff --git a/src/builders/protractor/index.js b/src/builders/protractor/index.js new file mode 100644 index 000000000..11934e422 --- /dev/null +++ b/src/builders/protractor/index.js @@ -0,0 +1,176 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = execute; +const architect_1 = require("@angular-devkit/architect"); +const core_1 = require("@angular-devkit/core"); +const node_path_1 = require("node:path"); +const url = __importStar(require("node:url")); +const utils_1 = require("../../utils"); +const error_1 = require("../../utils/error"); +function runProtractor(root, options) { + const additionalProtractorConfig = { + baseUrl: options.baseUrl, + specs: options.specs && options.specs.length ? options.specs : undefined, + suite: options.suite, + jasmineNodeOpts: { + grep: options.grep, + invertGrep: options.invertGrep, + }, + }; + // TODO: Protractor manages process.exit itself, so this target will allways quit the + // process. To work around this we run it in a subprocess. + // https://github.com/angular/protractor/issues/4160 + return (0, utils_1.runModuleAsObservableFork)(root, 'protractor/built/launcher', 'init', [ + (0, node_path_1.resolve)(root, options.protractorConfig), + additionalProtractorConfig, + ]).toPromise(); +} +async function updateWebdriver() { + // The webdriver-manager update command can only be accessed via a deep import. + const webdriverDeepImport = 'webdriver-manager/built/lib/cmds/update'; + let path; + try { + const protractorPath = require.resolve('protractor'); + path = require.resolve(webdriverDeepImport, { paths: [protractorPath] }); + } + catch (error) { + (0, error_1.assertIsError)(error); + if (error.code !== 'MODULE_NOT_FOUND') { + throw error; + } + } + if (!path) { + throw new Error(core_1.tags.stripIndents ` + Cannot automatically find webdriver-manager to update. + Update webdriver-manager manually and run 'ng e2e --no-webdriver-update' instead. + `); + } + const webdriverUpdate = await Promise.resolve(`${path}`).then(s => __importStar(require(s))); + // const webdriverUpdate = await import(path) as typeof import ('webdriver-manager/built/lib/cmds/update'); + // run `webdriver-manager update --standalone false --gecko false --quiet` + // if you change this, update the command comment in prev line + return webdriverUpdate.program.run({ + standalone: false, + gecko: false, + quiet: true, + }); +} +/** + * @experimental Direct usage of this function is considered experimental. + */ +async function execute(options, context) { + context.logger.warn('Protractor has reached end-of-life and is no longer supported by the Angular team. The `protractor` builder will be removed in a future Angular major version. For additional information and alternatives, please see https://blog.angular.dev/protractor-deprecation-update-august-2023-2beac7402ce0.'); + // ensure that only one of these options is used + if (options.devServerTarget && options.baseUrl) { + throw new Error(core_1.tags.stripIndents ` + The 'baseUrl' option cannot be used with 'devServerTarget'. + When present, 'devServerTarget' will be used to automatically setup 'baseUrl' for Protractor. + `); + } + if (options.webdriverUpdate) { + await updateWebdriver(); + } + let baseUrl = options.baseUrl; + let server; + try { + if (options.devServerTarget) { + const target = (0, architect_1.targetFromTargetString)(options.devServerTarget); + const serverOptions = await context.getTargetOptions(target); + const overrides = { + watch: false, + liveReload: false, + }; + if (options.host !== undefined) { + overrides.host = options.host; + } + else if (typeof serverOptions.host === 'string') { + options.host = serverOptions.host; + } + else { + options.host = overrides.host = 'localhost'; + } + if (options.port !== undefined) { + overrides.port = options.port; + } + else if (typeof serverOptions.port === 'number') { + options.port = serverOptions.port; + } + server = await context.scheduleTarget(target, overrides); + const result = await server.result; + if (!result.success) { + return { success: false }; + } + if (typeof serverOptions.publicHost === 'string') { + let publicHost = serverOptions.publicHost; + if (!/^\w+:\/\//.test(publicHost)) { + publicHost = `${serverOptions.ssl ? 'https' : 'http'}://${publicHost}`; + } + const clientUrl = url.parse(publicHost); + baseUrl = url.format(clientUrl); + } + else if (typeof result.baseUrl === 'string') { + baseUrl = result.baseUrl; + } + else if (typeof result.port === 'number') { + baseUrl = url.format({ + protocol: serverOptions.ssl ? 'https' : 'http', + hostname: options.host, + port: result.port.toString(), + }); + } + } + // Like the baseUrl in protractor config file when using the API we need to add + // a trailing slash when provide to the baseUrl. + if (baseUrl && !baseUrl.endsWith('/')) { + baseUrl += '/'; + } + return await runProtractor(context.workspaceRoot, { ...options, baseUrl }); + } + catch { + return { success: false }; + } + finally { + await server?.stop(); + } +} +exports.default = (0, architect_1.createBuilder)(execute); diff --git a/src/protractor/schema.d.ts b/src/builders/protractor/schema.d.ts similarity index 76% rename from src/protractor/schema.d.ts rename to src/builders/protractor/schema.d.ts index 44ddece7e..149af036b 100644 --- a/src/protractor/schema.d.ts +++ b/src/builders/protractor/schema.d.ts @@ -1,13 +1,15 @@ /** * Protractor target options for Build Facade. */ -export interface Schema { +export type Schema = { /** * Base URL for protractor to connect to. */ baseUrl?: string; /** - * Dev server target to run tests against. + * A dev-server builder target to run tests against in the format of + * `project:target[:configuration]`. You can also pass in more than one configuration name + * as a comma-separated list. Example: `project:target:production,staging`. */ devServerTarget?: string; /** @@ -42,4 +44,4 @@ export interface Schema { * Try to update webdriver. */ webdriverUpdate?: boolean; -} +}; diff --git a/src/builders/protractor/schema.js b/src/builders/protractor/schema.js new file mode 100644 index 000000000..4fb6d3d1b --- /dev/null +++ b/src/builders/protractor/schema.js @@ -0,0 +1,4 @@ +"use strict"; +// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE +// CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/protractor/schema.json b/src/builders/protractor/schema.json similarity index 84% rename from src/protractor/schema.json rename to src/builders/protractor/schema.json index c4fd14eff..286a315a9 100644 --- a/src/protractor/schema.json +++ b/src/builders/protractor/schema.json @@ -10,7 +10,7 @@ }, "devServerTarget": { "type": "string", - "description": "Dev server target to run tests against.", + "description": "A dev-server builder target to run tests against in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", "pattern": "^([^:\\s]+:[^:\\s]+(:[^\\s]+)?)?$" }, "grep": { @@ -54,7 +54,5 @@ } }, "additionalProperties": false, - "required": [ - "protractorConfig" - ] + "required": ["protractorConfig"] } diff --git a/src/builders/server/index.d.ts b/src/builders/server/index.d.ts new file mode 100644 index 000000000..810bf3d27 --- /dev/null +++ b/src/builders/server/index.d.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import { Observable } from 'rxjs'; +import webpack from 'webpack'; +import { ExecutionTransformer } from '../../transforms'; +import { Schema as ServerBuilderOptions } from './schema'; +/** + * @experimental Direct usage of this type is considered experimental. + */ +export type ServerBuilderOutput = BuilderOutput & { + baseOutputPath: string; + outputPath: string; + outputs: { + locale?: string; + path: string; + }[]; +}; +export type { ServerBuilderOptions }; +/** + * @experimental Direct usage of this function is considered experimental. + */ +export declare function execute(options: ServerBuilderOptions, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; +}): Observable; +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; diff --git a/src/builders/server/index.js b/src/builders/server/index.js new file mode 100644 index 000000000..4d97aef6a --- /dev/null +++ b/src/builders/server/index.js @@ -0,0 +1,223 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = execute; +const private_1 = require("@angular/build/private"); +const architect_1 = require("@angular-devkit/architect"); +const build_webpack_1 = require("@angular-devkit/build-webpack"); +const promises_1 = require("node:fs/promises"); +const path = __importStar(require("node:path")); +const rxjs_1 = require("rxjs"); +const webpack_1 = __importDefault(require("webpack")); +const configs_1 = require("../../tools/webpack/configs"); +const helpers_1 = require("../../tools/webpack/utils/helpers"); +const stats_1 = require("../../tools/webpack/utils/stats"); +const utils_1 = require("../../utils"); +const color_1 = require("../../utils/color"); +const copy_assets_1 = require("../../utils/copy-assets"); +const error_1 = require("../../utils/error"); +const i18n_inlining_1 = require("../../utils/i18n-inlining"); +const output_paths_1 = require("../../utils/output-paths"); +const spinner_1 = require("../../utils/spinner"); +const webpack_browser_config_1 = require("../../utils/webpack-browser-config"); +/** + * @experimental Direct usage of this function is considered experimental. + */ +function execute(options, context, transforms = {}) { + const root = context.workspaceRoot; + // Check Angular version. + (0, private_1.assertCompatibleAngularVersion)(root); + const baseOutputPath = path.resolve(root, options.outputPath); + let outputPaths; + return (0, rxjs_1.from)(initialize(options, context, transforms.webpackConfiguration)).pipe((0, rxjs_1.concatMap)(({ config, i18n, projectRoot, projectSourceRoot }) => { + return (0, build_webpack_1.runWebpack)(config, context, { + webpackFactory: require('webpack'), + logging: (stats, config) => { + if (options.verbose && config.stats !== false) { + const statsOptions = config.stats === true ? undefined : config.stats; + context.logger.info(stats.toString(statsOptions)); + } + }, + }).pipe((0, rxjs_1.concatMap)(async (output) => { + const { emittedFiles = [], outputPath, webpackStats, success } = output; + if (!webpackStats) { + throw new Error('Webpack stats build result is required.'); + } + if (!success) { + if ((0, stats_1.statsHasWarnings)(webpackStats)) { + context.logger.warn((0, stats_1.statsWarningsToString)(webpackStats, { colors: true })); + } + if ((0, stats_1.statsHasErrors)(webpackStats)) { + context.logger.error((0, stats_1.statsErrorsToString)(webpackStats, { colors: true })); + } + return output; + } + const spinner = new spinner_1.Spinner(); + spinner.enabled = options.progress !== false; + outputPaths = (0, output_paths_1.ensureOutputPaths)(baseOutputPath, i18n); + // Copy assets + if (!options.watch && options.assets?.length) { + spinner.start('Copying assets...'); + try { + await (0, copy_assets_1.copyAssets)((0, utils_1.normalizeAssetPatterns)(options.assets, context.workspaceRoot, projectRoot, projectSourceRoot), Array.from(outputPaths.values()), context.workspaceRoot); + spinner.succeed('Copying assets complete.'); + } + catch (err) { + spinner.fail(color_1.colors.redBright('Copying of assets failed.')); + (0, error_1.assertIsError)(err); + return { + ...output, + success: false, + error: 'Unable to copy assets: ' + err.message, + }; + } + } + if (i18n.shouldInline) { + const success = await (0, i18n_inlining_1.i18nInlineEmittedFiles)(context, emittedFiles, i18n, baseOutputPath, Array.from(outputPaths.values()), [], outputPath, options.i18nMissingTranslation); + if (!success) { + return { + ...output, + success: false, + }; + } + } + (0, stats_1.webpackStatsLogger)(context.logger, webpackStats, config); + return output; + })); + }), (0, rxjs_1.concatMap)(async (output) => { + if (!output.success) { + return output; + } + return { + ...output, + baseOutputPath, + outputs: (outputPaths && + [...outputPaths.entries()].map(([locale, path]) => ({ + locale, + path, + }))) || { + path: baseOutputPath, + }, + }; + })); +} +exports.default = (0, architect_1.createBuilder)(execute); +async function initialize(options, context, webpackConfigurationTransform) { + // Purge old build disk cache. + await (0, private_1.purgeStaleBuildCache)(context); + await checkTsConfigForPreserveWhitespacesSetting(context, options.tsConfig); + const browserslist = (await Promise.resolve().then(() => __importStar(require('browserslist')))).default; + const originalOutputPath = options.outputPath; + // Assets are processed directly by the builder except when watching + const adjustedOptions = options.watch ? options : { ...options, assets: [] }; + const { config, projectRoot, projectSourceRoot, i18n } = await (0, webpack_browser_config_1.generateI18nBrowserWebpackConfigFromContext)({ + ...adjustedOptions, + aot: true, + platform: 'server', + }, context, (wco) => { + // We use the platform to determine the JavaScript syntax output. + wco.buildOptions.supportedBrowsers ??= []; + wco.buildOptions.supportedBrowsers.push(...browserslist('maintained node versions')); + return [ + getPlatformServerExportsConfig(wco), + (0, configs_1.getCommonConfig)(wco), + (0, configs_1.getStylesConfig)(wco), + { + plugins: [ + new webpack_1.default.DefinePlugin({ + 'ngJitMode': false, + 'ngServerMode': true, + }), + ], + }, + ]; + }); + if (options.deleteOutputPath) { + await (0, utils_1.deleteOutputDir)(context.workspaceRoot, originalOutputPath); + } + const transformedConfig = (await webpackConfigurationTransform?.(config)) ?? config; + return { config: transformedConfig, i18n, projectRoot, projectSourceRoot }; +} +async function checkTsConfigForPreserveWhitespacesSetting(context, tsConfigPath) { + // We don't use the `readTsConfig` method on purpose here. + // To only catch cases were `preserveWhitespaces` is set directly in the `tsconfig.server.json`, + // which in the majority of cases will cause a mistmatch between client and server builds. + // Technically we should check if `tsconfig.server.json` and `tsconfig.app.json` values match. + // But: + // 1. It is not guaranteed that `tsconfig.app.json` is used to build the client side of this app. + // 2. There is no easy way to access the build build config from the server builder. + // 4. This will no longer be an issue with a single compilation model were the same tsconfig is used for both browser and server builds. + const content = await (0, promises_1.readFile)(path.join(context.workspaceRoot, tsConfigPath), 'utf-8'); + const { parse } = await Promise.resolve().then(() => __importStar(require('jsonc-parser'))); + const tsConfig = parse(content, [], { allowTrailingComma: true }); + if (tsConfig.angularCompilerOptions?.preserveWhitespaces !== undefined) { + context.logger.warn(`"preserveWhitespaces" was set in "${tsConfigPath}". ` + + 'Make sure that this setting is set consistently in both "tsconfig.server.json" for your server side ' + + 'and "tsconfig.app.json" for your client side. A mismatched value will cause hydration to break.\n' + + 'For more information see: https://angular.dev/guide/hydration#preserve-whitespaces-configuration'); + } +} +/** + * Add `@angular/platform-server` exports. + * This is needed so that DI tokens can be referenced and set at runtime outside of the bundle. + */ +function getPlatformServerExportsConfig(wco) { + // Add `@angular/platform-server` exports. + // This is needed so that DI tokens can be referenced and set at runtime outside of the bundle. + return { + module: { + rules: [ + { + loader: require.resolve('./platform-server-exports-loader'), + include: [path.resolve(wco.root, wco.buildOptions.main)], + options: { + angularSSRInstalled: (0, helpers_1.isPackageInstalled)(wco.root, '@angular/ssr'), + isZoneJsInstalled: (0, helpers_1.isPackageInstalled)(wco.root, 'zone.js'), + }, + }, + ], + }, + }; +} diff --git a/src/builders/server/platform-server-exports-loader.d.ts b/src/builders/server/platform-server-exports-loader.d.ts new file mode 100644 index 000000000..4f00e95a9 --- /dev/null +++ b/src/builders/server/platform-server-exports-loader.d.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +/** + * This loader is needed to add additional exports and is a workaround for a Webpack bug that doesn't + * allow exports from multiple files in the same entry. + * @see https://github.com/webpack/webpack/issues/15936. + */ +export default function (this: import('webpack').LoaderContext<{ + angularSSRInstalled: boolean; + isZoneJsInstalled: boolean; +}>, content: string, map: Parameters[1]): void; diff --git a/src/builders/server/platform-server-exports-loader.js b/src/builders/server/platform-server-exports-loader.js new file mode 100644 index 000000000..c88e3b12d --- /dev/null +++ b/src/builders/server/platform-server-exports-loader.js @@ -0,0 +1,34 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +/** + * This loader is needed to add additional exports and is a workaround for a Webpack bug that doesn't + * allow exports from multiple files in the same entry. + * @see https://github.com/webpack/webpack/issues/15936. + */ +function default_1(content, map) { + const { angularSSRInstalled, isZoneJsInstalled } = this.getOptions(); + let source = `${content} + + // EXPORTS added by @angular-devkit/build-angular + export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server'; + `; + if (angularSSRInstalled) { + source += ` + export { ɵgetRoutesFromAngularRouterConfig } from '@angular/ssr'; + `; + } + if (isZoneJsInstalled) { + source = `import 'zone.js/node'; + ${source}`; + } + this.callback(null, source, map); + return; +} diff --git a/src/builders/server/schema.d.ts b/src/builders/server/schema.d.ts new file mode 100644 index 000000000..34593d253 --- /dev/null +++ b/src/builders/server/schema.d.ts @@ -0,0 +1,229 @@ +export type Schema = { + /** + * List of static application assets. + */ + assets?: AssetPattern[]; + /** + * Enables advanced build optimizations. + */ + buildOptimizer?: boolean; + /** + * Delete the output path before building. + */ + deleteOutputPath?: boolean; + /** + * Customize the base path for the URLs of resources in 'index.html' and component + * stylesheets. This option is only necessary for specific deployment scenarios, such as + * with Angular Elements or when utilizing different CDN locations. + */ + deployUrl?: string; + /** + * Exclude the listed external dependencies from being bundled into the bundle. Instead, the + * created bundle relies on these dependencies to be available during runtime. + */ + externalDependencies?: string[]; + /** + * Extract all licenses in a separate file, in the case of production builds only. + */ + extractLicenses?: boolean; + /** + * Replace compilation source files with other compilation source files in the build. + */ + fileReplacements?: FileReplacement[]; + /** + * How to handle duplicate translations for i18n. + */ + i18nDuplicateTranslation?: I18NTranslation; + /** + * How to handle missing translations for i18n. + */ + i18nMissingTranslation?: I18NTranslation; + /** + * The stylesheet language to use for the application's inline component styles. + */ + inlineStyleLanguage?: InlineStyleLanguage; + /** + * Translate the bundles in one or more locales. + */ + localize?: Localize; + /** + * The name of the main entry-point file. + */ + main: string; + /** + * Use file name for lazy loaded chunks. + */ + namedChunks?: boolean; + /** + * Enables optimization of the build output. Including minification of scripts and styles, + * tree-shaking and dead-code elimination. For more information, see + * https://angular.dev/reference/configs/workspace-config#optimization-configuration. + */ + optimization?: OptimizationUnion; + /** + * Define the output filename cache-busting hashing mode. + */ + outputHashing?: OutputHashing; + /** + * Path where output will be placed. + */ + outputPath: string; + /** + * Enable and define the file watching poll time period in milliseconds. + */ + poll?: number; + /** + * Do not use the real path when resolving modules. If unset then will default to `true` if + * NodeJS option --preserve-symlinks is set. + */ + preserveSymlinks?: boolean; + /** + * Log progress to the console while building. + */ + progress?: boolean; + /** + * The path where style resources will be placed, relative to outputPath. + */ + resourcesOutputPath?: string; + /** + * Output source maps for scripts and styles. For more information, see + * https://angular.dev/reference/configs/workspace-config#source-map-configuration. + */ + sourceMap?: SourceMapUnion; + /** + * Generates a 'stats.json' file which can be analyzed using tools such as + * 'webpack-bundle-analyzer'. + */ + statsJson?: boolean; + /** + * Options to pass to style preprocessors + */ + stylePreprocessorOptions?: StylePreprocessorOptions; + /** + * The name of the TypeScript configuration file. + */ + tsConfig: string; + /** + * Generate a seperate bundle containing only vendor libraries. This option should only be + * used for development to reduce the incremental compilation time. + */ + vendorChunk?: boolean; + /** + * Adds more details to output logging. + */ + verbose?: boolean; + /** + * Run build when files change. + */ + watch?: boolean; +}; +export type AssetPattern = AssetPatternClass | string; +export type AssetPatternClass = { + /** + * Allow glob patterns to follow symlink directories. This allows subdirectories of the + * symlink to be searched. + */ + followSymlinks?: boolean; + /** + * The pattern to match. + */ + glob: string; + /** + * An array of globs to ignore. + */ + ignore?: string[]; + /** + * The input directory path in which to apply 'glob'. Defaults to the project root. + */ + input: string; + /** + * Absolute path within the output. + */ + output?: string; +}; +export type FileReplacement = { + replace?: string; + replaceWith?: string; + src?: string; + with?: string; +}; +/** + * How to handle duplicate translations for i18n. + * + * How to handle missing translations for i18n. + */ +export declare enum I18NTranslation { + Error = "error", + Ignore = "ignore", + Warning = "warning" +} +/** + * The stylesheet language to use for the application's inline component styles. + */ +export declare enum InlineStyleLanguage { + Css = "css", + Less = "less", + Sass = "sass", + Scss = "scss" +} +/** + * Translate the bundles in one or more locales. + */ +export type Localize = string[] | boolean; +/** + * Enables optimization of the build output. Including minification of scripts and styles, + * tree-shaking and dead-code elimination. For more information, see + * https://angular.dev/reference/configs/workspace-config#optimization-configuration. + */ +export type OptimizationUnion = boolean | OptimizationClass; +export type OptimizationClass = { + /** + * Enables optimization of the scripts output. + */ + scripts?: boolean; + /** + * Enables optimization of the styles output. + */ + styles?: boolean; +}; +/** + * Define the output filename cache-busting hashing mode. + */ +export declare enum OutputHashing { + All = "all", + Bundles = "bundles", + Media = "media", + None = "none" +} +/** + * Output source maps for scripts and styles. For more information, see + * https://angular.dev/reference/configs/workspace-config#source-map-configuration. + */ +export type SourceMapUnion = boolean | SourceMapClass; +export type SourceMapClass = { + /** + * Output source maps used for error reporting tools. + */ + hidden?: boolean; + /** + * Output source maps for all scripts. + */ + scripts?: boolean; + /** + * Output source maps for all styles. + */ + styles?: boolean; + /** + * Resolve vendor packages source maps. + */ + vendor?: boolean; +}; +/** + * Options to pass to style preprocessors + */ +export type StylePreprocessorOptions = { + /** + * Paths to include. Paths will be resolved to workspace root. + */ + includePaths?: string[]; +}; diff --git a/src/builders/server/schema.js b/src/builders/server/schema.js new file mode 100644 index 000000000..a632d2cf2 --- /dev/null +++ b/src/builders/server/schema.js @@ -0,0 +1,36 @@ +"use strict"; +// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE +// CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OutputHashing = exports.InlineStyleLanguage = exports.I18NTranslation = void 0; +/** + * How to handle duplicate translations for i18n. + * + * How to handle missing translations for i18n. + */ +var I18NTranslation; +(function (I18NTranslation) { + I18NTranslation["Error"] = "error"; + I18NTranslation["Ignore"] = "ignore"; + I18NTranslation["Warning"] = "warning"; +})(I18NTranslation || (exports.I18NTranslation = I18NTranslation = {})); +/** + * The stylesheet language to use for the application's inline component styles. + */ +var InlineStyleLanguage; +(function (InlineStyleLanguage) { + InlineStyleLanguage["Css"] = "css"; + InlineStyleLanguage["Less"] = "less"; + InlineStyleLanguage["Sass"] = "sass"; + InlineStyleLanguage["Scss"] = "scss"; +})(InlineStyleLanguage || (exports.InlineStyleLanguage = InlineStyleLanguage = {})); +/** + * Define the output filename cache-busting hashing mode. + */ +var OutputHashing; +(function (OutputHashing) { + OutputHashing["All"] = "all"; + OutputHashing["Bundles"] = "bundles"; + OutputHashing["Media"] = "media"; + OutputHashing["None"] = "none"; +})(OutputHashing || (exports.OutputHashing = OutputHashing = {})); diff --git a/src/server/schema.json b/src/builders/server/schema.json similarity index 59% rename from src/server/schema.json rename to src/builders/server/schema.json index 58ccfa577..2375e1176 100644 --- a/src/server/schema.json +++ b/src/builders/server/schema.json @@ -1,9 +1,17 @@ { - "$schema": "http://json-schema.org/schema", - "id": "BuildAngularWebpackServerSchema", + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "BuildAngularWebpackServerSchema", "title": "Universal Target", "type": "object", "properties": { + "assets": { + "type": "array", + "description": "List of static application assets.", + "default": [], + "items": { + "$ref": "#/definitions/assetPattern" + } + }, "main": { "type": "string", "description": "The name of the main entry-point file." @@ -13,12 +21,18 @@ "default": "tsconfig.app.json", "description": "The name of the TypeScript configuration file." }, + "inlineStyleLanguage": { + "description": "The stylesheet language to use for the application's inline component styles.", + "type": "string", + "default": "css", + "enum": ["css", "less", "sass", "scss"] + }, "stylePreprocessorOptions": { "description": "Options to pass to style preprocessors", "type": "object", "properties": { "includePaths": { - "description": "Paths to include. Paths will be resolved to project root.", + "description": "Paths to include. Paths will be resolved to workspace root.", "type": "array", "items": { "type": "string" @@ -29,8 +43,9 @@ "additionalProperties": false }, "optimization": { - "description": "Enables optimization of the build output.", - "x-user-analytics": 16, + "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking and dead-code elimination. For more information, see https://angular.dev/reference/configs/workspace-config#optimization-configuration.", + "default": true, + "x-user-analytics": "ep.ng_optimization", "oneOf": [ { "type": "object", @@ -54,7 +69,7 @@ ] }, "fileReplacements": { - "description": "Replace files with other files in the build.", + "description": "Replace compilation source files with other compilation source files in the build.", "type": "array", "items": { "$ref": "#/definitions/fileReplacement" @@ -67,34 +82,33 @@ }, "resourcesOutputPath": { "type": "string", - "description": "The path where style resources will be placed, relative to outputPath.", - "default": "" + "description": "The path where style resources will be placed, relative to outputPath." }, "sourceMap": { - "description": "Output sourcemaps.", - "default": true, + "description": "Output source maps for scripts and styles. For more information, see https://angular.dev/reference/configs/workspace-config#source-map-configuration.", + "default": false, "oneOf": [ { "type": "object", "properties": { "scripts": { "type": "boolean", - "description": "Output sourcemaps for all scripts.", + "description": "Output source maps for all scripts.", "default": true }, "styles": { "type": "boolean", - "description": "Output sourcemaps for all styles.", + "description": "Output source maps for all styles.", "default": true }, "hidden": { "type": "boolean", - "description": "Output sourcemaps used for error reporting tools.", + "description": "Output source maps used for error reporting tools.", "default": false }, "vendor": { "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", + "description": "Resolve vendor packages source maps.", "default": false } }, @@ -107,7 +121,12 @@ }, "deployUrl": { "type": "string", - "description": "URL where files will be deployed." + "description": "Customize the base path for the URLs of resources in 'index.html' and component stylesheets. This option is only necessary for specific deployment scenarios, such as with Angular Elements or when utilizing different CDN locations." + }, + "vendorChunk": { + "type": "boolean", + "description": "Generate a seperate bundle containing only vendor libraries. This option should only be used for development to reduce the incremental compilation time.", + "default": false }, "verbose": { "type": "boolean", @@ -116,22 +135,8 @@ }, "progress": { "type": "boolean", - "description": "Log progress to the console while building." - }, - "i18nFile": { - "type": "string", - "description": "Localization file to use for i18n.", - "x-deprecated": "Use 'locales' object in the project metadata instead." - }, - "i18nFormat": { - "type": "string", - "description": "Format of the localization file specified with --i18n-file.", - "x-deprecated": "No longer needed as the format will be determined automatically." - }, - "i18nLocale": { - "type": "string", - "description": "Locale to use for i18n.", - "x-deprecated": "Use 'localize' instead." + "description": "Log progress to the console while building.", + "default": true }, "i18nMissingTranslation": { "type": "string", @@ -139,7 +144,14 @@ "enum": ["warning", "error", "ignore"], "default": "warning" }, + "i18nDuplicateTranslation": { + "type": "string", + "description": "How to handle duplicate translations for i18n.", + "enum": ["warning", "error", "ignore"], + "default": "warning" + }, "localize": { + "description": "Translate the bundles in one or more locales.", "oneOf": [ { "type": "boolean", @@ -160,12 +172,7 @@ "type": "string", "description": "Define the output filename cache-busting hashing mode.", "default": "none", - "enum": [ - "none", - "all", - "media", - "bundles" - ] + "enum": ["none", "all", "media", "bundles"] }, "deleteOutputPath": { "type": "boolean", @@ -181,31 +188,15 @@ "description": "Extract all licenses in a separate file, in the case of production builds only.", "default": true }, - "showCircularDependencies": { + "buildOptimizer": { "type": "boolean", - "description": "Show circular dependency warnings on builds.", + "description": "Enables advanced build optimizations.", "default": true }, "namedChunks": { "type": "boolean", "description": "Use file name for lazy loaded chunks.", - "default": true - }, - "bundleDependencies": { - "description": "Which external dependencies to bundle into the bundle. By default, all of node_modules will be bundled.", - "default": true, - "oneOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "enum": [ - "none", - "all" - ] - } - ] + "default": false }, "externalDependencies": { "description": "Exclude the listed external dependencies from being bundled into the bundle. Instead, the created bundle relies on these dependencies to be available during runtime.", @@ -220,20 +211,6 @@ "description": "Generates a 'stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", "default": false }, - "forkTypeChecker": { - "type": "boolean", - "description": "Run the TypeScript type checker in a forked process.", - "default": true - }, - "lazyModules": { - "description": "List of additional NgModule files that will be lazy loaded. Lazy router modules will be discovered automatically.", - "type": "array", - "items": { - "type": "string" - }, - "x-deprecated": "'SystemJsNgModuleLoader' is deprecated, and this is part of its usage. Use 'import()' syntax instead.", - "default": [] - }, "watch": { "type": "boolean", "description": "Run build when files change.", @@ -245,45 +222,78 @@ } }, "additionalProperties": false, - "required": [ - "outputPath", - "main", - "tsConfig" - ], + "required": ["outputPath", "main", "tsConfig"], "definitions": { + "assetPattern": { + "oneOf": [ + { + "type": "object", + "properties": { + "followSymlinks": { + "type": "boolean", + "default": false, + "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched." + }, + "glob": { + "type": "string", + "description": "The pattern to match." + }, + "input": { + "type": "string", + "description": "The input directory path in which to apply 'glob'. Defaults to the project root." + }, + "ignore": { + "description": "An array of globs to ignore.", + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "string", + "default": "", + "description": "Absolute path within the output." + } + }, + "additionalProperties": false, + "required": ["glob", "input"] + }, + { + "type": "string" + } + ] + }, "fileReplacement": { "oneOf": [ { "type": "object", "properties": { "src": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?[jt])sx?|json)$" }, "replaceWith": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?[jt])sx?|json)$" } }, "additionalProperties": false, - "required": [ - "src", - "replaceWith" - ] + "required": ["src", "replaceWith"] }, { "type": "object", "properties": { "replace": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?[jt])sx?|json)$" }, "with": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?[jt])sx?|json)$" } }, "additionalProperties": false, - "required": [ - "replace", - "with" - ] + "required": ["replace", "with"] } ] } diff --git a/src/builders/ssr-dev-server/index.d.ts b/src/builders/ssr-dev-server/index.d.ts new file mode 100644 index 000000000..94ed97d7a --- /dev/null +++ b/src/builders/ssr-dev-server/index.d.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import { json, logging } from '@angular-devkit/core'; +import { Observable } from 'rxjs'; +import { Schema } from './schema'; +export type SSRDevServerBuilderOptions = Schema; +export type SSRDevServerBuilderOutput = BuilderOutput & { + baseUrl?: string; + port?: string; +}; +export declare function execute(options: SSRDevServerBuilderOptions, context: BuilderContext): Observable; +export declare function log({ stderr, stdout }: { + stderr: string | undefined; + stdout: string | undefined; +}, logger: logging.LoggerApi): void; +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; diff --git a/src/builders/ssr-dev-server/index.js b/src/builders/ssr-dev-server/index.js new file mode 100644 index 000000000..c58b674ce --- /dev/null +++ b/src/builders/ssr-dev-server/index.js @@ -0,0 +1,314 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = execute; +exports.log = log; +const private_1 = require("@angular/build/private"); +const architect_1 = require("@angular-devkit/architect"); +const core_1 = require("@angular-devkit/core"); +const node_path_1 = require("node:path"); +const url = __importStar(require("node:url")); +const rxjs_1 = require("rxjs"); +const utils_1 = require("./utils"); +/** Log messages to ignore and not rely to the logger */ +const IGNORED_STDOUT_MESSAGES = [ + 'server listening on', + 'Angular is running in development mode. Call enableProdMode() to enable production mode.', +]; +function execute(options, context) { + let browserSync; + try { + browserSync = require('browser-sync'); + } + catch { + return (0, rxjs_1.of)({ + success: false, + error: + // eslint-disable-next-line max-len + 'Required dependency `browser-sync` is not installed, most likely you need to run `npm install browser-sync --save-dev` in your project.', + }); + } + const bsInstance = browserSync.create(); + const browserTarget = (0, architect_1.targetFromTargetString)(options.browserTarget); + const serverTarget = (0, architect_1.targetFromTargetString)(options.serverTarget); + const getBaseUrl = (bs) => `${bs.getOption('scheme')}://${bs.getOption('host')}:${bs.getOption('port')}`; + const browserTargetRun = context.scheduleTarget(browserTarget, { + watch: options.watch, + progress: options.progress, + verbose: options.verbose, + // Disable bundle budgets are these are not meant to be used with a dev-server as this will add extra JavaScript for live-reloading. + budgets: [], + }); + const serverTargetRun = context.scheduleTarget(serverTarget, { + watch: options.watch, + progress: options.progress, + verbose: options.verbose, + }); + context.logger.error(core_1.tags.stripIndents ` + **************************************************************************************** + This is a simple server for use in testing or debugging Angular applications locally. + It hasn't been reviewed for security issues. + + DON'T USE IT FOR PRODUCTION! + **************************************************************************************** + `); + return (0, rxjs_1.zip)(browserTargetRun, serverTargetRun, (0, utils_1.getAvailablePort)()).pipe((0, rxjs_1.switchMap)(([br, sr, nodeServerPort]) => { + return (0, rxjs_1.combineLatest)([br.output, sr.output]).pipe( + // This is needed so that if both server and browser emit close to each other + // we only emit once. This typically happens on the first build. + (0, rxjs_1.debounceTime)(120), (0, rxjs_1.switchMap)(([b, s]) => { + if (!s.success || !b.success) { + return (0, rxjs_1.of)([b, s]); + } + return startNodeServer(s, nodeServerPort, context.logger, !!options.inspect).pipe((0, rxjs_1.map)(() => [b, s]), (0, rxjs_1.catchError)((err) => { + context.logger.error(`A server error has occurred.\n${mapErrorToMessage(err)}`); + return rxjs_1.EMPTY; + })); + }), (0, rxjs_1.map)(([b, s]) => [ + { + success: b.success && s.success, + error: b.error || s.error, + }, + nodeServerPort, + ]), (0, rxjs_1.tap)(([builderOutput]) => { + if (builderOutput.success) { + context.logger.info('\nCompiled successfully.'); + } + }), (0, rxjs_1.debounce)(([builderOutput]) => builderOutput.success && !options.inspect + ? (0, utils_1.waitUntilServerIsListening)(nodeServerPort) + : rxjs_1.EMPTY), (0, rxjs_1.finalize)(() => { + void br.stop(); + void sr.stop(); + })); + }), (0, rxjs_1.concatMap)(([builderOutput, nodeServerPort]) => { + if (!builderOutput.success) { + return (0, rxjs_1.of)(builderOutput); + } + if (bsInstance.active) { + bsInstance.reload(); + return (0, rxjs_1.of)(builderOutput); + } + else { + return (0, rxjs_1.from)(initBrowserSync(bsInstance, nodeServerPort, options, context)).pipe((0, rxjs_1.tap)((bs) => { + const baseUrl = getBaseUrl(bs); + context.logger.info(core_1.tags.oneLine ` + ** + Angular Universal Live Development Server is listening on ${baseUrl}, + open your browser on ${baseUrl} + ** + `); + }), (0, rxjs_1.map)(() => builderOutput)); + } + }), (0, rxjs_1.map)((builderOutput) => ({ + success: builderOutput.success, + error: builderOutput.error, + baseUrl: getBaseUrl(bsInstance), + port: bsInstance.getOption('port'), + })), (0, rxjs_1.finalize)(() => { + if (bsInstance) { + bsInstance.exit(); + bsInstance.cleanup(); + } + }), (0, rxjs_1.catchError)((error) => (0, rxjs_1.of)({ + success: false, + error: mapErrorToMessage(error), + }))); +} +// Logs output to the terminal. +// Removes any trailing new lines from the output. +function log({ stderr, stdout }, logger) { + if (stderr) { + // Strip the webpack scheme (webpack://) from error log. + logger.error(stderr.replace(/\n?$/, '').replace(/webpack:\/\//g, '.')); + } + if (stdout && !IGNORED_STDOUT_MESSAGES.some((x) => stdout.includes(x))) { + logger.info(stdout.replace(/\n?$/, '')); + } +} +function startNodeServer(serverOutput, port, logger, inspectMode = false) { + const outputPath = serverOutput.outputPath; + const path = (0, node_path_1.join)(outputPath, 'main.js'); + const env = { ...process.env, PORT: '' + port }; + const args = ['--enable-source-maps', `"${path}"`]; + if (inspectMode) { + args.unshift('--inspect-brk'); + } + 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. + (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)(), + // Emit a signal after the process has been started + (0, rxjs_1.startWith)(undefined)); +} +async function initBrowserSync(browserSyncInstance, nodeServerPort, options, context) { + if (browserSyncInstance.active) { + return browserSyncInstance; + } + const { port: browserSyncPort, open, host, publicHost, proxyConfig } = options; + const bsPort = browserSyncPort || (await (0, utils_1.getAvailablePort)()); + const bsOptions = { + proxy: { + target: `localhost:${nodeServerPort}`, + proxyOptions: { + xfwd: true, + }, + proxyRes: [ + (proxyRes) => { + if ('headers' in proxyRes) { + proxyRes.headers['cache-control'] = undefined; + } + }, + ], + // proxyOptions is not in the typings + }, + host, + port: bsPort, + ui: false, + server: false, + notify: false, + ghostMode: false, + logLevel: options.verbose ? 'debug' : 'silent', + open, + https: getSslConfig(context.workspaceRoot, options), + }; + const publicHostNormalized = publicHost && publicHost.endsWith('/') + ? publicHost.substring(0, publicHost.length - 1) + : publicHost; + if (publicHostNormalized) { + const { protocol, hostname, port, pathname } = url.parse(publicHostNormalized); + const defaultSocketIoPath = '/browser-sync/socket.io'; + const defaultNamespace = '/browser-sync'; + const hasPathname = !!(pathname && pathname !== '/'); + const namespace = hasPathname ? pathname + defaultNamespace : defaultNamespace; + const path = hasPathname ? pathname + defaultSocketIoPath : defaultSocketIoPath; + bsOptions.socket = { + namespace, + path, + domain: url.format({ + protocol, + hostname, + port, + }), + }; + // When having a pathname we also need to create a reverse proxy because socket.io + // will be listening on: 'http://localhost:4200/ssr/browser-sync/socket.io' + // However users will typically have a reverse proxy that will redirect all matching requests + // ex: http://testinghost.com/ssr -> http://localhost:4200 which will result in a 404. + if (hasPathname) { + const { createProxyMiddleware } = await Promise.resolve().then(() => __importStar(require('http-proxy-middleware'))); + // Remove leading slash + bsOptions.scriptPath = (p) => p.substring(1); + bsOptions.middleware = [ + createProxyMiddleware({ + pathFilter: defaultSocketIoPath, + target: url.format({ + protocol: 'http', + hostname: host, + port: bsPort, + pathname: path, + }), + ws: true, + logger: { + info: () => { }, + warn: () => { }, + error: () => { }, + }, + }), + ]; + } + } + if (proxyConfig) { + if (!bsOptions.middleware) { + bsOptions.middleware = []; + } + else if (!Array.isArray(bsOptions.middleware)) { + bsOptions.middleware = [bsOptions.middleware]; + } + bsOptions.middleware = [ + ...bsOptions.middleware, + ...(await getProxyConfig(context.workspaceRoot, proxyConfig)), + ]; + } + return new Promise((resolve, reject) => { + browserSyncInstance.init(bsOptions, (error, bs) => { + if (error) { + reject(error); + } + else { + resolve(bs); + } + }); + }); +} +function mapErrorToMessage(error) { + if (error instanceof Error) { + return error.message; + } + if (typeof error === 'string') { + return error; + } + return ''; +} +function getSslConfig(root, options) { + const { ssl, sslCert, sslKey } = options; + if (ssl && sslCert && sslKey) { + return { + key: (0, node_path_1.resolve)(root, sslKey), + cert: (0, node_path_1.resolve)(root, sslCert), + }; + } + return ssl; +} +async function getProxyConfig(root, proxyConfig) { + const proxy = await (0, private_1.loadProxyConfiguration)(root, proxyConfig); + if (!proxy) { + return []; + } + const { createProxyMiddleware } = await Promise.resolve().then(() => __importStar(require('http-proxy-middleware'))); + return Object.entries(proxy).map(([key, context]) => { + const filterRegExp = new RegExp(key); + return createProxyMiddleware({ + pathFilter: (pathname) => filterRegExp.test(pathname), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...context, + }); + }); +} +exports.default = (0, architect_1.createBuilder)(execute); diff --git a/src/builders/ssr-dev-server/schema.d.ts b/src/builders/ssr-dev-server/schema.d.ts new file mode 100644 index 000000000..70a046643 --- /dev/null +++ b/src/builders/ssr-dev-server/schema.d.ts @@ -0,0 +1,64 @@ +/** + * SSR Dev Server target options for Build Facade. + */ +export type Schema = { + /** + * Browser target to build. + */ + browserTarget: string; + /** + * Host to listen on. + */ + host?: string; + /** + * Launch the development server in inspector mode and listen on address and port + * '127.0.0.1:9229'. + */ + inspect?: boolean; + /** + * Opens the url in default browser. + */ + open?: boolean; + /** + * Port to start the development server at. Default is 4200. Pass 0 to get a dynamically + * assigned port. + */ + port?: number; + /** + * Log progress to the console while building. + */ + progress?: boolean; + /** + * Proxy configuration file. + */ + proxyConfig?: string; + /** + * The URL that the browser client should use to connect to the development server. Use for + * a complex dev server setup, such as one with reverse proxies. + */ + publicHost?: string; + /** + * Server target to build. + */ + serverTarget: string; + /** + * Serve using HTTPS. + */ + ssl?: boolean; + /** + * SSL certificate to use for serving HTTPS. + */ + sslCert?: string; + /** + * SSL key to use for serving HTTPS. + */ + sslKey?: string; + /** + * Adds more details to output logging. + */ + verbose?: boolean; + /** + * Rebuild on change. + */ + watch?: boolean; +}; diff --git a/src/builders/ssr-dev-server/schema.js b/src/builders/ssr-dev-server/schema.js new file mode 100644 index 000000000..4fb6d3d1b --- /dev/null +++ b/src/builders/ssr-dev-server/schema.js @@ -0,0 +1,4 @@ +"use strict"; +// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE +// CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/builders/ssr-dev-server/schema.json b/src/builders/ssr-dev-server/schema.json new file mode 100644 index 000000000..648f2f5dc --- /dev/null +++ b/src/builders/ssr-dev-server/schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "SSR Dev Server Target", + "description": "SSR Dev Server target options for Build Facade.", + "type": "object", + "properties": { + "browserTarget": { + "type": "string", + "description": "Browser target to build.", + "pattern": ".+:.+(:.+)?" + }, + "serverTarget": { + "type": "string", + "description": "Server target to build.", + "pattern": ".+:.+(:.+)?" + }, + "host": { + "type": "string", + "description": "Host to listen on.", + "default": "localhost" + }, + "port": { + "type": "number", + "default": 4200, + "description": "Port to start the development server at. Default is 4200. Pass 0 to get a dynamically assigned port." + }, + "watch": { + "type": "boolean", + "description": "Rebuild on change.", + "default": true + }, + "publicHost": { + "type": "string", + "description": "The URL that the browser client should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies." + }, + "open": { + "type": "boolean", + "description": "Opens the url in default browser.", + "default": false, + "alias": "o" + }, + "progress": { + "type": "boolean", + "description": "Log progress to the console while building." + }, + "inspect": { + "type": "boolean", + "description": "Launch the development server in inspector mode and listen on address and port '127.0.0.1:9229'.", + "default": false + }, + "ssl": { + "type": "boolean", + "description": "Serve using HTTPS.", + "default": false + }, + "sslKey": { + "type": "string", + "description": "SSL key to use for serving HTTPS." + }, + "sslCert": { + "type": "string", + "description": "SSL certificate to use for serving HTTPS." + }, + "proxyConfig": { + "type": "string", + "description": "Proxy configuration file." + }, + "verbose": { + "type": "boolean", + "description": "Adds more details to output logging." + } + }, + "additionalProperties": false, + "required": ["browserTarget", "serverTarget"] +} diff --git a/src/builders/ssr-dev-server/utils.d.ts b/src/builders/ssr-dev-server/utils.d.ts new file mode 100644 index 000000000..62a666743 --- /dev/null +++ b/src/builders/ssr-dev-server/utils.d.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { SpawnOptions } from 'node:child_process'; +import { Observable } from 'rxjs'; +export declare function getAvailablePort(): Promise; +export declare function spawnAsObservable(command: string, args?: string[], options?: SpawnOptions): Observable<{ + stdout?: string; + stderr?: string; +}>; +export declare function waitUntilServerIsListening(port: number, host?: string): Observable; diff --git a/src/builders/ssr-dev-server/utils.js b/src/builders/ssr-dev-server/utils.js new file mode 100644 index 000000000..2d7e98cfc --- /dev/null +++ b/src/builders/ssr-dev-server/utils.js @@ -0,0 +1,73 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getAvailablePort = getAvailablePort; +exports.spawnAsObservable = spawnAsObservable; +exports.waitUntilServerIsListening = waitUntilServerIsListening; +const node_child_process_1 = require("node:child_process"); +const node_net_1 = require("node:net"); +const rxjs_1 = require("rxjs"); +const tree_kill_1 = __importDefault(require("tree-kill")); +function getAvailablePort() { + return new Promise((resolve, reject) => { + const server = (0, node_net_1.createServer)(); + server + .unref() + .on('error', reject) + .listen(0, () => { + const { port } = server.address(); + server.close(() => resolve(port)); + }); + }); +} +function spawnAsObservable(command, args = [], options = {}) { + return new rxjs_1.Observable((obs) => { + const proc = (0, node_child_process_1.spawn)(command, args, options); + if (proc.stdout) { + proc.stdout.on('data', (data) => obs.next({ stdout: data.toString() })); + } + if (proc.stderr) { + proc.stderr.on('data', (data) => obs.next({ stderr: data.toString() })); + } + proc + .on('error', (err) => obs.error(err)) + .on('close', (code) => { + if (code !== 0) { + obs.error(new Error(`${command} exited with ${code} code.`)); + } + obs.complete(); + }); + return () => { + if (!proc.killed && proc.pid) { + (0, tree_kill_1.default)(proc.pid, 'SIGTERM'); + } + }; + }); +} +function waitUntilServerIsListening(port, host) { + const allowedErrorCodes = ['ECONNREFUSED', 'ECONNRESET']; + return new rxjs_1.Observable((obs) => { + const client = (0, node_net_1.createConnection)({ host, port }, () => { + obs.next(undefined); + obs.complete(); + }).on('error', (err) => obs.error(err)); + return () => { + if (!client.destroyed) { + client.destroy(); + } + }; + }).pipe((0, rxjs_1.retryWhen)((err) => err.pipe((0, rxjs_1.mergeMap)((error, attempts) => { + return attempts > 10 || !allowedErrorCodes.includes(error.code) + ? (0, rxjs_1.throwError)(error) + : (0, rxjs_1.timer)(100 * (attempts * 1)); + })))); +} diff --git a/src/builders/web-test-runner/builder-status-warnings.d.ts b/src/builders/web-test-runner/builder-status-warnings.d.ts new file mode 100644 index 000000000..f1b752b31 --- /dev/null +++ b/src/builders/web-test-runner/builder-status-warnings.d.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { BuilderContext } from '@angular-devkit/architect'; +import { Schema as WtrBuilderOptions } from './schema'; +/** Logs a warning for any unsupported options specified. */ +export declare function logBuilderStatusWarnings(options: WtrBuilderOptions, ctx: BuilderContext): void; diff --git a/src/builders/web-test-runner/builder-status-warnings.js b/src/builders/web-test-runner/builder-status-warnings.js new file mode 100644 index 000000000..5d76e1466 --- /dev/null +++ b/src/builders/web-test-runner/builder-status-warnings.js @@ -0,0 +1,45 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.logBuilderStatusWarnings = logBuilderStatusWarnings; +const UNSUPPORTED_OPTIONS = [ + 'main', + 'assets', + 'scripts', + 'styles', + 'inlineStyleLanguage', + 'stylePreprocessorOptions', + 'sourceMap', + 'progress', + 'poll', + 'preserveSymlinks', + 'browsers', + 'codeCoverage', + 'codeCoverageExclude', + 'fileReplacements', + 'webWorkerTsConfig', + 'watch', +]; +/** Logs a warning for any unsupported options specified. */ +function logBuilderStatusWarnings(options, ctx) { + // Validate supported options + for (const unsupportedOption of UNSUPPORTED_OPTIONS) { + const value = options[unsupportedOption]; + if (value === undefined || value === false) { + continue; + } + if (Array.isArray(value) && value.length === 0) { + continue; + } + if (typeof value === 'object' && Object.keys(value).length === 0) { + continue; + } + ctx.logger.warn(`The '${unsupportedOption}' option is not yet supported by this builder.`); + } +} diff --git a/src/builders/web-test-runner/index.d.ts b/src/builders/web-test-runner/index.d.ts new file mode 100644 index 000000000..4ed0d42db --- /dev/null +++ b/src/builders/web-test-runner/index.d.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Schema } from './schema'; +declare const _default: import("@angular-devkit/architect").Builder; +export default _default; diff --git a/src/builders/web-test-runner/index.js b/src/builders/web-test-runner/index.js new file mode 100644 index 000000000..4accd0de5 --- /dev/null +++ b/src/builders/web-test-runner/index.js @@ -0,0 +1,161 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const private_1 = require("@angular/build/private"); +const architect_1 = require("@angular-devkit/architect"); +const node_crypto_1 = require("node:crypto"); +const promises_1 = __importDefault(require("node:fs/promises")); +const node_module_1 = require("node:module"); +const node_path_1 = __importDefault(require("node:path")); +const test_files_1 = require("../../utils/test-files"); +const schema_1 = require("../browser-esbuild/schema"); +const builder_status_warnings_1 = require("./builder-status-warnings"); +const options_1 = require("./options"); +const write_test_files_1 = require("./write-test-files"); +exports.default = (0, architect_1.createBuilder)(async (schema, ctx) => { + ctx.logger.warn('NOTE: The Web Test Runner builder is currently EXPERIMENTAL and not ready for production use.'); + (0, builder_status_warnings_1.logBuilderStatusWarnings)(schema, ctx); + // Dynamic import `@web/test-runner` from the user's workspace. As an optional peer dep, it may not be installed + // and may not be resolvable from `@angular-devkit/build-angular`. + const require = (0, node_module_1.createRequire)(`${ctx.workspaceRoot}/`); + let wtr; + try { + wtr = require('@web/test-runner'); + } + catch { + return { + success: false, + // TODO(dgp1130): Display a more accurate message for non-NPM users. + error: 'Web Test Runner is not installed, most likely you need to run `npm install @web/test-runner --save-dev` in your project.', + }; + } + const options = (0, options_1.normalizeOptions)(schema); + const testDir = node_path_1.default.join(ctx.workspaceRoot, 'dist/test-out', (0, node_crypto_1.randomUUID)()); + // Parallelize startup work. + const [testFiles] = await Promise.all([ + // Glob for files to test. + (0, test_files_1.findTestFiles)(options.include, options.exclude, ctx.workspaceRoot), + // Clean build output path. + promises_1.default.rm(testDir, { recursive: true, force: true }), + ]); + // Build the tests and abort on any build failure. + const buildOutput = await buildTests(testFiles, testDir, options, ctx); + if (buildOutput.kind === private_1.ResultKind.Failure) { + return { success: false }; + } + else if (buildOutput.kind !== private_1.ResultKind.Full) { + return { + success: false, + error: 'A full build result is required from the application builder.', + }; + } + // Write test files + await (0, write_test_files_1.writeTestFiles)(buildOutput.files, testDir); + // Run the built tests. + return await runTests(wtr, testDir, options); +}); +/** Build all the given test files and write the result to the given output path. */ +async function buildTests(testFiles, outputPath, options, ctx) { + const entryPoints = new Set([ + ...testFiles, + 'jasmine-core/lib/jasmine-core/jasmine.js', + '@angular-devkit/build-angular/src/builders/web-test-runner/jasmine_runner.js', + ]); + // Extract `zone.js/testing` to a separate entry point because it needs to be loaded after Jasmine. + const [polyfills, hasZoneTesting] = extractZoneTesting(options.polyfills); + if (hasZoneTesting) { + entryPoints.add('zone.js/testing'); + } + // Build tests with `application` builder, using test files as entry points. + // Also bundle in Jasmine and the Jasmine runner script, which need to share chunked dependencies. + const buildOutput = await first((0, private_1.buildApplicationInternal)({ + entryPoints, + tsConfig: options.tsConfig, + outputPath, + aot: options.aot, + index: false, + outputHashing: schema_1.OutputHashing.None, + optimization: false, + externalDependencies: [ + // Resolved by `@web/test-runner` at runtime with dynamically generated code. + '@web/test-runner-core', + ], + sourceMap: { + scripts: true, + styles: true, + vendor: true, + }, + polyfills, + }, ctx)); + return buildOutput; +} +function extractZoneTesting(polyfills) { + const polyfillsWithoutZoneTesting = polyfills.filter((polyfill) => polyfill !== 'zone.js/testing'); + const hasZoneTesting = polyfills.length !== polyfillsWithoutZoneTesting.length; + return [polyfillsWithoutZoneTesting, hasZoneTesting]; +} +/** Run Web Test Runner on the given directory of bundled JavaScript tests. */ +async function runTests(wtr, testDir, options) { + const testPagePath = node_path_1.default.resolve(__dirname, 'test_page.html'); + const testPage = await promises_1.default.readFile(testPagePath, 'utf8'); + const runner = await wtr.startTestRunner({ + config: { + rootDir: testDir, + files: [ + `${testDir}/**/*.js`, + `!${testDir}/polyfills.js`, + `!${testDir}/chunk-*.js`, + `!${testDir}/jasmine.js`, + `!${testDir}/jasmine_runner.js`, + `!${testDir}/testing.js`, // `zone.js/testing` + ], + testFramework: { + config: { + defaultTimeoutInterval: 5_000, + }, + }, + nodeResolve: true, + port: 9876, + watch: options.watch ?? false, + testRunnerHtml: (_testFramework, _config) => testPage, + }, + readCliArgs: false, + readFileConfig: false, + autoExitProcess: false, + }); + if (!runner) { + throw new Error('Failed to start Web Test Runner.'); + } + // Wait for the tests to complete and stop the runner. + const passed = (await once(runner, 'finished')); + await runner.stop(); + // No need to return error messages because Web Test Runner already printed them to the console. + return { success: passed }; +} +/** Returns the first item yielded by the given generator and cancels the execution. */ +async function first(generator) { + for await (const value of generator) { + return value; + } + throw new Error('Expected generator to emit at least once.'); +} +/** Listens for a single emission of an event and returns the value emitted. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function once(emitter, event) { + return new Promise((resolve) => { + const onEmit = (arg) => { + emitter.off(event, onEmit); + resolve(arg); + }; + emitter.on(event, onEmit); + }); +} diff --git a/src/builders/web-test-runner/jasmine_runner.js b/src/builders/web-test-runner/jasmine_runner.js new file mode 100644 index 000000000..0775ec09b --- /dev/null +++ b/src/builders/web-test-runner/jasmine_runner.js @@ -0,0 +1,91 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { NgModule } from '@angular/core'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; +import { + getConfig, + sessionFailed, + sessionFinished, + sessionStarted, +} from '@web/test-runner-core/browser/session.js'; + +/** Executes Angular Jasmine tests in the given environment and reports the results to Web Test Runner. */ +export async function runJasmineTests(jasmineEnv) { + const allSpecs = []; + const failedSpecs = []; + + jasmineEnv.addReporter({ + specDone(result) { + const expectations = [...result.passedExpectations, ...result.failedExpectations]; + allSpecs.push(...expectations.map((e) => ({ name: e.fullName, passed: e.passed }))); + + for (const e of result.failedExpectations) { + const message = `${result.fullName}\n${e.message}\n${e.stack}`; + // eslint-disable-next-line no-console + console.error(message); + failedSpecs.push({ + message, + name: e.fullName, + stack: e.stack, + expected: e.expected, + actual: e.actual, + }); + } + }, + + async jasmineDone(result) { + // eslint-disable-next-line no-console + console.log(`Tests ${result.overallStatus}!`); + await sessionFinished({ + passed: result.overallStatus === 'passed', + errors: failedSpecs, + testResults: { + name: '', + suites: [], + tests: allSpecs, + }, + }); + }, + }); + + await sessionStarted(); + + // 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. + const { testFile, testFrameworkConfig } = await getConfig(); + const config = { defaultTimeoutInterval: 60_000, ...(testFrameworkConfig ?? {}) }; + + // eslint-disable-next-line no-undef + jasmine.DEFAULT_TIMEOUT_INTERVAL = config.defaultTimeoutInterval; + + @NgModule({ + providers: [typeof window.Zone !== 'undefined' ? provideZoneChangeDetection() : []], + }) + class TestModule {} + + // Initialize `TestBed` automatically for users. This assumes we already evaluated `zone.js/testing`. + getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }); + + // Load the test file and evaluate it. + try { + // eslint-disable-next-line no-undef + await import(new URL(testFile, document.baseURI).href); + + // Execute the test functions. + // eslint-disable-next-line no-undef + jasmineEnv.execute(); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + await sessionFailed(err); + } +} diff --git a/src/builders/web-test-runner/options.d.ts b/src/builders/web-test-runner/options.d.ts new file mode 100644 index 000000000..1e99eab82 --- /dev/null +++ b/src/builders/web-test-runner/options.d.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Schema as WtrBuilderSchema } from './schema'; +/** + * Options supported for the Web Test Runner builder. The schema is an approximate + * representation of the options type, but this is a more precise version. + */ +export type WtrBuilderOptions = Overwrite; +type Overwrite = Omit & Overrides; +/** + * Normalizes input options validated by the schema to a more precise and useful + * options type in {@link WtrBuilderOptions}. + */ +export declare function normalizeOptions(schema: WtrBuilderSchema): WtrBuilderOptions; +export {}; diff --git a/src/builders/web-test-runner/options.js b/src/builders/web-test-runner/options.js new file mode 100644 index 000000000..5678c059c --- /dev/null +++ b/src/builders/web-test-runner/options.js @@ -0,0 +1,25 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeOptions = normalizeOptions; +/** + * Normalizes input options validated by the schema to a more precise and useful + * options type in {@link WtrBuilderOptions}. + */ +function normalizeOptions(schema) { + return { + ...schema, + // Options with default values can't actually be null, even if the types say so. + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + include: schema.include, + exclude: schema.exclude, + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + polyfills: typeof schema.polyfills === 'string' ? [schema.polyfills] : schema.polyfills ?? [], + }; +} diff --git a/src/karma/schema.d.ts b/src/builders/web-test-runner/schema.d.ts similarity index 55% rename from src/karma/schema.d.ts rename to src/builders/web-test-runner/schema.d.ts index b713dbc62..d65713fa3 100644 --- a/src/karma/schema.d.ts +++ b/src/builders/web-test-runner/schema.d.ts @@ -1,7 +1,11 @@ /** - * Karma target options for Build Facade. + * Web Test Runner target options for Build Facade. */ -export interface Schema { +export type Schema = { + /** + * Run tests using Ahead of Time compilation. + */ + aot?: boolean; /** * List of static application assets. */ @@ -19,39 +23,38 @@ export interface Schema { */ codeCoverageExclude?: string[]; /** - * Defines the build environment. - * @deprecated This option has no effect. + * Globs of files to exclude, relative to the project root. */ - environment?: string; + exclude?: string[]; /** - * Replace files with other files in the build. + * Replace compilation source files with other compilation source files in the build. */ fileReplacements?: FileReplacement[]; /** - * Globs of files to include, relative to workspace or project root. + * Globs of files to include, relative to project root. * There are 2 special cases: * - when a path to directory is provided, all spec files ending ".spec.@(ts|tsx)" will be * included * - when a path to a file is provided, and a matching spec file exists it will be included - * instead + * instead. */ include?: string[]; /** - * The name of the Karma configuration file. + * The stylesheet language to use for the application's inline component styles. */ - karmaConfig: string; + inlineStyleLanguage?: InlineStyleLanguage; /** * The name of the main entry-point file. */ - main: string; + main?: string; /** * Enable and define the file watching poll time period in milliseconds. */ poll?: number; /** - * The name of the polyfills file. + * Polyfills to be included in the build. */ - polyfills?: string; + polyfills?: Polyfills; /** * Do not use the real path when resolving modules. If unset then will default to `true` if * NodeJS option --preserve-symlinks is set. @@ -61,16 +64,13 @@ export interface Schema { * Log progress to the console while building. */ progress?: boolean; - /** - * Karma reporters to use. Directly passed to the karma runner. - */ - reporters?: string[]; /** * Global scripts to be included in the build. */ - scripts?: ExtraEntryPoint[]; + scripts?: ScriptElement[]; /** - * Output sourcemaps. + * Output source maps for scripts and styles. For more information, see + * https://angular.dev/reference/configs/workspace-config#source-map-configuration. */ sourceMap?: SourceMapUnion; /** @@ -80,7 +80,7 @@ export interface Schema { /** * Global styles to be included in the build. */ - styles?: ExtraEntryPoint[]; + styles?: StyleElement[]; /** * The name of the TypeScript configuration file. */ @@ -93,9 +93,9 @@ export interface Schema { * TypeScript configuration for Web Worker modules. */ webWorkerTsConfig?: string; -} -export declare type AssetPattern = AssetPatternClass | string; -export interface AssetPatternClass { +}; +export type AssetPattern = AssetPatternClass | string; +export type AssetPatternClass = { /** * The pattern to match. */ @@ -111,16 +111,29 @@ export interface AssetPatternClass { /** * Absolute path within the output. */ - output: string; -} -export interface FileReplacement { + output?: string; +}; +export type FileReplacement = { replace?: string; replaceWith?: string; src?: string; with?: string; +}; +/** + * The stylesheet language to use for the application's inline component styles. + */ +export declare enum InlineStyleLanguage { + Css = "css", + Less = "less", + Sass = "sass", + Scss = "scss" } -export declare type ExtraEntryPoint = ExtraEntryPointClass | string; -export interface ExtraEntryPointClass { +/** + * Polyfills to be included in the build. + */ +export type Polyfills = string[] | string; +export type ScriptElement = ScriptClass | string; +export type ScriptClass = { /** * The bundle name for this extra entry point. */ @@ -133,35 +146,47 @@ export interface ExtraEntryPointClass { * The file to include. */ input: string; - /** - * If the bundle will be lazy loaded. - */ - lazy?: boolean; -} +}; /** - * Output sourcemaps. + * Output source maps for scripts and styles. For more information, see + * https://angular.dev/reference/configs/workspace-config#source-map-configuration. */ -export declare type SourceMapUnion = boolean | SourceMapClass; -export interface SourceMapClass { +export type SourceMapUnion = boolean | SourceMapClass; +export type SourceMapClass = { /** - * Output sourcemaps for all scripts. + * Output source maps for all scripts. */ scripts?: boolean; /** - * Output sourcemaps for all styles. + * Output source maps for all styles. */ styles?: boolean; /** - * Resolve vendor packages sourcemaps. + * Resolve vendor packages source maps. */ vendor?: boolean; -} +}; /** * Options to pass to style preprocessors */ -export interface StylePreprocessorOptions { +export type StylePreprocessorOptions = { /** - * Paths to include. Paths will be resolved to project root. + * Paths to include. Paths will be resolved to workspace root. */ includePaths?: string[]; -} +}; +export type StyleElement = StyleClass | string; +export type StyleClass = { + /** + * The bundle name for this extra entry point. + */ + bundleName?: string; + /** + * If the bundle will be referenced in the HTML file. + */ + inject?: boolean; + /** + * The file to include. + */ + input: string; +}; diff --git a/src/builders/web-test-runner/schema.js b/src/builders/web-test-runner/schema.js new file mode 100644 index 000000000..08e82f7dd --- /dev/null +++ b/src/builders/web-test-runner/schema.js @@ -0,0 +1,15 @@ +"use strict"; +// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE +// CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). +Object.defineProperty(exports, "__esModule", { value: true }); +exports.InlineStyleLanguage = void 0; +/** + * The stylesheet language to use for the application's inline component styles. + */ +var InlineStyleLanguage; +(function (InlineStyleLanguage) { + InlineStyleLanguage["Css"] = "css"; + InlineStyleLanguage["Less"] = "less"; + InlineStyleLanguage["Sass"] = "sass"; + InlineStyleLanguage["Scss"] = "scss"; +})(InlineStyleLanguage || (exports.InlineStyleLanguage = InlineStyleLanguage = {})); diff --git a/src/karma/schema.json b/src/builders/web-test-runner/schema.json similarity index 55% rename from src/karma/schema.json rename to src/builders/web-test-runner/schema.json index 56677c066..c922e7688 100644 --- a/src/karma/schema.json +++ b/src/builders/web-test-runner/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema", - "title": "Karma Target", - "description": "Karma target options for Build Facade.", + "title": "Web Test Runner Target", + "description": "Web Test Runner target options for Build Facade.", "type": "object", "properties": { "main": { @@ -12,13 +12,23 @@ "type": "string", "description": "The name of the TypeScript configuration file." }, - "karmaConfig": { - "type": "string", - "description": "The name of the Karma configuration file." - }, "polyfills": { - "type": "string", - "description": "The name of the polyfills file." + "description": "Polyfills to be included in the build.", + "oneOf": [ + { + "type": "array", + "description": "A list of polyfills to include in the build. Can be a full path for a file, relative to the current workspace or module specifier. Example: 'zone.js'.", + "items": { + "type": "string", + "uniqueItems": true + }, + "default": [] + }, + { + "type": "string", + "description": "The full path for the polyfills file, relative to the current workspace or a module specifier. Example: 'zone.js'." + } + ] }, "assets": { "type": "array", @@ -33,7 +43,35 @@ "type": "array", "default": [], "items": { - "$ref": "#/definitions/extraEntryPoint" + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include.", + "pattern": "\\.[cm]?jsx?$" + }, + "bundleName": { + "type": "string", + "pattern": "^[\\w\\-.]*$", + "description": "The bundle name for this extra entry point." + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true + } + }, + "additionalProperties": false, + "required": ["input"] + }, + { + "type": "string", + "description": "The file to include.", + "pattern": "\\.[cm]?jsx?$" + } + ] } }, "styles": { @@ -41,15 +79,49 @@ "type": "array", "default": [], "items": { - "$ref": "#/definitions/extraEntryPoint" + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include.", + "pattern": "\\.(?:css|scss|sass|less)$" + }, + "bundleName": { + "type": "string", + "pattern": "^[\\w\\-.]*$", + "description": "The bundle name for this extra entry point." + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true + } + }, + "additionalProperties": false, + "required": ["input"] + }, + { + "type": "string", + "description": "The file to include.", + "pattern": "\\.(?:css|scss|sass|less)$" + } + ] } }, + "inlineStyleLanguage": { + "description": "The stylesheet language to use for the application's inline component styles.", + "type": "string", + "default": "css", + "enum": ["css", "less", "sass", "scss"] + }, "stylePreprocessorOptions": { "description": "Options to pass to style preprocessors", "type": "object", "properties": { "includePaths": { - "description": "Paths to include. Paths will be resolved to project root.", + "description": "Paths to include. Paths will be resolved to workspace root.", "type": "array", "items": { "type": "string" @@ -59,20 +131,24 @@ }, "additionalProperties": false }, - "environment": { - "type": "string", - "description": "Defines the build environment.", - "x-deprecated": "This option has no effect." - }, "include": { "type": "array", "items": { "type": "string" }, - "description": "Globs of files to include, relative to workspace or project root. \nThere are 2 special cases:\n - when a path to directory is provided, all spec files ending \".spec.@(ts|tsx)\" will be included\n - when a path to a file is provided, and a matching spec file exists it will be included instead" + "default": ["**/*.spec.ts"], + "description": "Globs of files to include, relative to project root. \nThere are 2 special cases:\n - when a path to directory is provided, all spec files ending \".spec.@(ts|tsx)\" will be included\n - when a path to a file is provided, and a matching spec file exists it will be included instead." + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "Globs of files to exclude, relative to the project root." }, "sourceMap": { - "description": "Output sourcemaps.", + "description": "Output source maps for scripts and styles. For more information, see https://angular.dev/reference/configs/workspace-config#source-map-configuration.", "default": true, "oneOf": [ { @@ -80,17 +156,17 @@ "properties": { "scripts": { "type": "boolean", - "description": "Output sourcemaps for all scripts.", + "description": "Output source maps for all scripts.", "default": true }, "styles": { "type": "boolean", - "description": "Output sourcemaps for all styles.", + "description": "Output source maps for all styles.", "default": true }, "vendor": { "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", + "description": "Resolve vendor packages source maps.", "default": false } }, @@ -103,7 +179,8 @@ }, "progress": { "type": "boolean", - "description": "Log progress to the console while building." + "description": "Log progress to the console while building.", + "default": true }, "watch": { "type": "boolean", @@ -135,7 +212,7 @@ "default": [] }, "fileReplacements": { - "description": "Replace files with other files in the build.", + "description": "Replace compilation source files with other compilation source files in the build.", "type": "array", "items": { "oneOf": [ @@ -150,10 +227,7 @@ } }, "additionalProperties": false, - "required": [ - "src", - "replaceWith" - ] + "required": ["src", "replaceWith"] }, { "type": "object", @@ -166,33 +240,24 @@ } }, "additionalProperties": false, - "required": [ - "replace", - "with" - ] + "required": ["replace", "with"] } ] }, "default": [] }, - "reporters": { - "type": "array", - "description": "Karma reporters to use. Directly passed to the karma runner.", - "items": { - "type": "string" - } - }, "webWorkerTsConfig": { "type": "string", "description": "TypeScript configuration for Web Worker modules." + }, + "aot": { + "type": "boolean", + "description": "Run tests using Ahead of Time compilation.", + "default": false } }, "additionalProperties": false, - "required": [ - "main", - "tsConfig", - "karmaConfig" - ], + "required": ["tsConfig"], "definitions": { "assetPattern": { "oneOf": [ @@ -209,6 +274,7 @@ }, "output": { "type": "string", + "default": "", "description": "Absolute path within the output." }, "ignore": { @@ -220,52 +286,12 @@ } }, "additionalProperties": false, - "required": [ - "glob", - "input", - "output" - ] + "required": ["glob", "input"] }, { "type": "string" } ] - }, - "extraEntryPoint": { - "oneOf": [ - { - "type": "object", - "properties": { - "input": { - "type": "string", - "description": "The file to include." - }, - "bundleName": { - "type": "string", - "description": "The bundle name for this extra entry point." - }, - "lazy": { - "type": "boolean", - "description": "If the bundle will be lazy loaded.", - "default": false, - "x-deprecated": "Use 'inject' option with 'false' value instead." - }, - "inject": { - "type": "boolean", - "description": "If the bundle will be referenced in the HTML file.", - "default": true - } - }, - "additionalProperties": false, - "required": [ - "input" - ] - }, - { - "type": "string", - "description": "The file to include." - } - ] } } -} \ No newline at end of file +} diff --git a/src/builders/web-test-runner/test_page.html b/src/builders/web-test-runner/test_page.html new file mode 100644 index 000000000..9cff64dcc --- /dev/null +++ b/src/builders/web-test-runner/test_page.html @@ -0,0 +1,40 @@ + + + + + Unit tests + + + + + diff --git a/src/builders/web-test-runner/write-test-files.d.ts b/src/builders/web-test-runner/write-test-files.d.ts new file mode 100644 index 000000000..10084bc8e --- /dev/null +++ b/src/builders/web-test-runner/write-test-files.d.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { ResultFile } from '@angular/build/private'; +export declare function writeTestFiles(files: Record, testDir: string): Promise; diff --git a/src/builders/web-test-runner/write-test-files.js b/src/builders/web-test-runner/write-test-files.js new file mode 100644 index 000000000..1513bedff --- /dev/null +++ b/src/builders/web-test-runner/write-test-files.js @@ -0,0 +1,41 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.writeTestFiles = writeTestFiles; +const build_1 = require("@angular/build"); +const private_1 = require("@angular/build/private"); +const promises_1 = __importDefault(require("node:fs/promises")); +const node_path_1 = __importDefault(require("node:path")); +async function writeTestFiles(files, testDir) { + const directoryExists = new Set(); + // Writes the test related output files to disk and ensures the containing directories are present + await (0, private_1.emitFilesToDisk)(Object.entries(files), async ([filePath, file]) => { + if (file.type !== build_1.BuildOutputFileType.Browser && file.type !== build_1.BuildOutputFileType.Media) { + return; + } + const fullFilePath = node_path_1.default.join(testDir, filePath); + // Ensure output subdirectories exist + const fileBasePath = node_path_1.default.dirname(fullFilePath); + if (fileBasePath && !directoryExists.has(fileBasePath)) { + await promises_1.default.mkdir(fileBasePath, { recursive: true }); + directoryExists.add(fileBasePath); + } + if (file.origin === 'memory') { + // Write file contents + await promises_1.default.writeFile(fullFilePath, file.contents); + } + else { + // Copy file contents + await promises_1.default.copyFile(file.inputPath, fullFilePath, promises_1.default.constants.COPYFILE_FICLONE); + } + }); +} diff --git a/src/dev-server/index.d.ts b/src/dev-server/index.d.ts deleted file mode 100644 index 2e97f17e5..000000000 --- a/src/dev-server/index.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { BuilderContext } from '@angular-devkit/architect'; -import { DevServerBuildOutput, WebpackLoggingCallback } from '@angular-devkit/build-webpack'; -import { json, logging } from '@angular-devkit/core'; -import { Observable } from 'rxjs'; -import * as webpack from 'webpack'; -import * as WebpackDevServer from 'webpack-dev-server'; -import { IndexHtmlTransform } from '../angular-cli-files/utilities/index-file/write-index-html'; -import { Schema as BrowserBuilderSchema } from '../browser/schema'; -import { ExecutionTransformer } from '../transforms'; -import { Schema } from './schema'; -export declare type DevServerBuilderOptions = Schema & json.JsonObject; -export declare type DevServerBuilderOutput = DevServerBuildOutput & { - baseUrl: string; -}; -/** - * Reusable implementation of the build angular webpack dev server builder. - * @param options Dev Server options. - * @param context The build context. - * @param transforms A map of transforms that can be used to hook into some logic (such as - * transforming webpack configuration before passing it to webpack). - */ -export declare function serveWebpackBrowser(options: DevServerBuilderOptions, context: BuilderContext, transforms?: { - webpackConfiguration?: ExecutionTransformer; - logging?: WebpackLoggingCallback; - indexHtml?: IndexHtmlTransform; -}): Observable; -/** - * Create a webpack configuration for the dev server. - * @param workspaceRoot The root of the workspace. This comes from the context. - * @param serverOptions DevServer options, based on the dev server input schema. - * @param browserOptions Browser builder options. See the browser builder from this package. - * @param logger A generic logger to use for showing warnings. - * @returns A webpack dev-server configuration. - */ -export declare function buildServerConfig(workspaceRoot: string, serverOptions: DevServerBuilderOptions, browserOptions: BrowserBuilderSchema, logger: logging.LoggerApi): WebpackDevServer.Configuration; -/** - * Resolve and build a URL _path_ that will be the root of the server. This resolved base href and - * deploy URL from the browser options and returns a path from the root. - * @param serverOptions The server options that were passed to the server builder. - * @param browserOptions The browser options that were passed to the browser builder. - * @param logger A generic logger to use for showing warnings. - */ -export declare function buildServePath(serverOptions: DevServerBuilderOptions, browserOptions: BrowserBuilderSchema, logger: logging.LoggerApi): string; -declare const _default: import("@angular-devkit/architect/src/internal").Builder; -export default _default; diff --git a/src/dev-server/index.js b/src/dev-server/index.js deleted file mode 100644 index b75893dfa..000000000 --- a/src/dev-server/index.js +++ /dev/null @@ -1,534 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.buildServePath = exports.buildServerConfig = exports.serveWebpackBrowser = void 0; -const architect_1 = require("@angular-devkit/architect"); -const build_webpack_1 = require("@angular-devkit/build-webpack"); -const core_1 = require("@angular-devkit/core"); -const node_1 = require("@angular-devkit/core/node"); -const fs_1 = require("fs"); -const path = require("path"); -const rxjs_1 = require("rxjs"); -const operators_1 = require("rxjs/operators"); -const ts = require("typescript"); -const url = require("url"); -const webpack = require("webpack"); -const index_html_webpack_plugin_1 = require("../angular-cli-files/plugins/index-html-webpack-plugin"); -const check_port_1 = require("../angular-cli-files/utilities/check-port"); -const package_chunk_sort_1 = require("../angular-cli-files/utilities/package-chunk-sort"); -const read_tsconfig_1 = require("../angular-cli-files/utilities/read-tsconfig"); -const browser_1 = require("../browser"); -const utils_1 = require("../utils"); -const cache_path_1 = require("../utils/cache-path"); -const process_bundle_1 = require("../utils/process-bundle"); -const version_1 = require("../utils/version"); -const webpack_browser_config_1 = require("../utils/webpack-browser-config"); -const open = require('open'); -const devServerBuildOverriddenKeys = [ - 'watch', - 'optimization', - 'aot', - 'sourceMap', - 'vendorChunk', - 'commonChunk', - 'baseHref', - 'progress', - 'poll', - 'verbose', - 'deployUrl', -]; -/** - * Reusable implementation of the build angular webpack dev server builder. - * @param options Dev Server options. - * @param context The build context. - * @param transforms A map of transforms that can be used to hook into some logic (such as - * transforming webpack configuration before passing it to webpack). - */ -// tslint:disable-next-line: no-big-function -function serveWebpackBrowser(options, context, transforms = {}) { - // Check Angular version. - version_1.assertCompatibleAngularVersion(context.workspaceRoot, context.logger); - const browserTarget = architect_1.targetFromTargetString(options.browserTarget); - const root = context.workspaceRoot; - let first = true; - const host = new node_1.NodeJsSyncHost(); - const loggingFn = transforms.logging || browser_1.createBrowserLoggingCallback(!!options.verbose, context.logger); - async function setup() { - // Get the browser configuration from the target name. - const rawBrowserOptions = await context.getTargetOptions(browserTarget); - // Override options we need to override, if defined. - const overrides = Object.keys(options) - .filter(key => options[key] !== undefined && devServerBuildOverriddenKeys.includes(key)) - .reduce((previous, key) => ({ - ...previous, - [key]: options[key], - }), {}); - // In dev server we should not have budgets because of extra libs such as socks-js - overrides.budgets = undefined; - const browserName = await context.getBuilderNameForTarget(browserTarget); - const browserOptions = await context.validateOptions({ ...rawBrowserOptions, ...overrides }, browserName); - const { config, projectRoot, i18n } = await browser_1.buildBrowserWebpackConfigFromContext(browserOptions, context, host, true); - let webpackConfig = config; - const tsConfig = read_tsconfig_1.readTsconfig(browserOptions.tsConfig, context.workspaceRoot); - if (i18n.shouldInline && tsConfig.options.enableIvy !== false) { - if (i18n.inlineLocales.size > 1) { - throw new Error('The development server only supports localizing a single locale per build'); - } - await setupLocalize(i18n, browserOptions, webpackConfig); - } - const port = await check_port_1.checkPort(options.port || 0, options.host || 'localhost', 4200); - const webpackDevServerConfig = (webpackConfig.devServer = buildServerConfig(root, options, browserOptions, context.logger)); - if (transforms.webpackConfiguration) { - webpackConfig = await transforms.webpackConfiguration(webpackConfig); - } - return { - browserOptions, - webpackConfig, - webpackDevServerConfig, - port, - projectRoot, - }; - } - return rxjs_1.from(setup()).pipe(operators_1.switchMap(({ browserOptions, webpackConfig, webpackDevServerConfig, port, projectRoot }) => { - options.port = port; - // Resolve public host and client address. - let clientAddress = url.parse(`${options.ssl ? 'https' : 'http'}://0.0.0.0:0`); - if (options.publicHost) { - let publicHost = options.publicHost; - if (!/^\w+:\/\//.test(publicHost)) { - publicHost = `${options.ssl ? 'https' : 'http'}://${publicHost}`; - } - clientAddress = url.parse(publicHost); - options.publicHost = clientAddress.host; - } - // Add live reload config. - if (options.liveReload) { - _addLiveReload(options, browserOptions, webpackConfig, clientAddress, context.logger); - } - else if (options.hmr) { - context.logger.warn('Live reload is disabled. HMR option ignored.'); - } - webpackConfig.plugins = [...(webpackConfig.plugins || [])]; - if (!options.watch) { - // There's no option to turn off file watching in webpack-dev-server, but - // we can override the file watcher instead. - webpackConfig.plugins.push({ - // tslint:disable-next-line:no-any - apply: (compiler) => { - compiler.hooks.afterEnvironment.tap('angular-cli', () => { - compiler.watchFileSystem = { watch: () => { } }; - }); - }, - }); - } - if (browserOptions.index) { - const { scripts = [], styles = [], baseHref, tsConfig } = browserOptions; - const { options: compilerOptions } = read_tsconfig_1.readTsconfig(tsConfig, context.workspaceRoot); - const target = compilerOptions.target || ts.ScriptTarget.ES5; - const buildBrowserFeatures = new utils_1.BuildBrowserFeatures(projectRoot, target); - const entrypoints = package_chunk_sort_1.generateEntryPoints({ scripts, styles }); - const moduleEntrypoints = buildBrowserFeatures.isDifferentialLoadingNeeded() - ? package_chunk_sort_1.generateEntryPoints({ scripts: [], styles }) - : []; - webpackConfig.plugins.push(new index_html_webpack_plugin_1.IndexHtmlWebpackPlugin({ - input: path.resolve(root, webpack_browser_config_1.getIndexInputFile(browserOptions)), - output: webpack_browser_config_1.getIndexOutputFile(browserOptions), - baseHref, - moduleEntrypoints, - entrypoints, - deployUrl: browserOptions.deployUrl, - sri: browserOptions.subresourceIntegrity, - noModuleEntrypoints: ['polyfills-es5'], - postTransform: transforms.indexHtml, - crossOrigin: browserOptions.crossOrigin, - lang: browserOptions.i18nLocale, - })); - } - const normalizedOptimization = utils_1.normalizeOptimization(browserOptions.optimization); - if (normalizedOptimization.scripts || normalizedOptimization.styles) { - context.logger.error(core_1.tags.stripIndents ` - **************************************************************************************** - This is a simple server for use in testing or debugging Angular applications locally. - It hasn't been reviewed for security issues. - - DON'T USE IT FOR PRODUCTION! - **************************************************************************************** - `); - } - return build_webpack_1.runWebpackDevServer(webpackConfig, context, { - logging: loggingFn, - webpackFactory: require('webpack'), - webpackDevServerFactory: require('webpack-dev-server'), - }).pipe(operators_1.map(buildEvent => { - // Resolve serve address. - const serverAddress = url.format({ - protocol: options.ssl ? 'https' : 'http', - hostname: options.host === '0.0.0.0' ? 'localhost' : options.host, - pathname: webpackDevServerConfig.publicPath, - port: buildEvent.port, - }); - if (first) { - first = false; - context.logger.info(core_1.tags.oneLine ` - ** - Angular Live Development Server is listening on ${options.host}:${buildEvent.port}, - open your browser on ${serverAddress} - ** - `); - if (options.open) { - open(serverAddress); - } - } - if (buildEvent.success) { - context.logger.info(': Compiled successfully.'); - } - return { ...buildEvent, baseUrl: serverAddress }; - })); - })); -} -exports.serveWebpackBrowser = serveWebpackBrowser; -async function setupLocalize(i18n, browserOptions, webpackConfig) { - const locale = [...i18n.inlineLocales][0]; - const localeDescription = i18n.locales[locale]; - const { plugins, diagnostics } = await process_bundle_1.createI18nPlugins(locale, localeDescription && localeDescription.translation, browserOptions.i18nMissingTranslation || 'ignore'); - // Modify main entrypoint to include locale data - if (localeDescription && - localeDescription.dataPath && - typeof webpackConfig.entry === 'object' && - !Array.isArray(webpackConfig.entry) && - webpackConfig.entry['main']) { - if (Array.isArray(webpackConfig.entry['main'])) { - webpackConfig.entry['main'].unshift(localeDescription.dataPath); - } - else { - webpackConfig.entry['main'] = [localeDescription.dataPath, webpackConfig.entry['main']]; - } - } - // Get the insertion point for the i18n babel loader rule - // This is currently dependent on the rule order/construction in common.ts - // A future refactor of the webpack configuration definition will improve this situation - // tslint:disable-next-line: no-non-null-assertion - const rules = webpackConfig.module.rules; - const index = rules.findIndex(r => r.enforce === 'pre'); - if (index === -1) { - throw new Error('Invalid internal webpack configuration'); - } - const i18nRule = { - test: /\.(?:m?js|ts)$/, - enforce: 'post', - use: [ - { - loader: require.resolve('babel-loader'), - options: { - babelrc: false, - configFile: false, - compact: false, - cacheCompression: false, - cacheDirectory: cache_path_1.findCachePath('babel-loader'), - cacheIdentifier: JSON.stringify({ - buildAngular: require('../../package.json').version, - locale, - translationIntegrity: localeDescription && localeDescription.integrity, - }), - plugins, - parserOpts: { - plugins: ['dynamicImport'], - }, - }, - }, - ], - }; - rules.splice(index, 0, i18nRule); - // Add a plugin to inject the i18n diagnostics - // tslint:disable-next-line: no-non-null-assertion - webpackConfig.plugins.push({ - apply: (compiler) => { - compiler.hooks.thisCompilation.tap('build-angular', compilation => { - compilation.hooks.finishModules.tap('build-angular', () => { - if (!diagnostics) { - return; - } - for (const diagnostic of diagnostics.messages) { - if (diagnostic.type === 'error') { - compilation.errors.push(diagnostic.message); - } - else { - compilation.warnings.push(diagnostic.message); - } - } - diagnostics.messages.length = 0; - }); - }); - }, - }); -} -/** - * Create a webpack configuration for the dev server. - * @param workspaceRoot The root of the workspace. This comes from the context. - * @param serverOptions DevServer options, based on the dev server input schema. - * @param browserOptions Browser builder options. See the browser builder from this package. - * @param logger A generic logger to use for showing warnings. - * @returns A webpack dev-server configuration. - */ -function buildServerConfig(workspaceRoot, serverOptions, browserOptions, logger) { - // Check that the host is either localhost or prints out a message. - if (serverOptions.host - && !/^127\.\d+\.\d+\.\d+/g.test(serverOptions.host) - && serverOptions.host !== 'localhost') { - logger.warn(core_1.tags.stripIndent ` - WARNING: This is a simple server for use in testing or debugging Angular applications - locally. It hasn't been reviewed for security issues. - - Binding this server to an open connection can result in compromising your application or - computer. Using a different host than the one passed to the "--host" flag might result in - websocket connection issues. You might need to use "--disableHostCheck" if that's the - case. - `); - } - if (serverOptions.disableHostCheck) { - logger.warn(core_1.tags.oneLine ` - WARNING: Running a server with --disable-host-check is a security risk. - See https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a - for more information. - `); - } - const servePath = buildServePath(serverOptions, browserOptions, logger); - const { styles, scripts } = utils_1.normalizeOptimization(browserOptions.optimization); - const config = { - host: serverOptions.host, - port: serverOptions.port, - headers: { 'Access-Control-Allow-Origin': '*' }, - historyApiFallback: !!browserOptions.index && { - index: `${servePath}/${webpack_browser_config_1.getIndexOutputFile(browserOptions)}`, - disableDotRule: true, - htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], - rewrites: [ - { - from: new RegExp(`^(?!${servePath})/.*`), - to: context => url.format(context.parsedUrl), - }, - ], - }, - stats: false, - compress: styles || scripts, - watchOptions: { - // Using just `--poll` will result in a value of 0 which is very likely not the intention - // A value of 0 is falsy and will disable polling rather then enable - // 500 ms is a sensible default in this case - poll: serverOptions.poll === 0 ? 500 : serverOptions.poll, - ignored: serverOptions.poll === undefined ? undefined : /[\\\/]node_modules[\\\/]/, - }, - https: serverOptions.ssl, - overlay: { - errors: !(styles || scripts), - warnings: false, - }, - // inline is always false, because we add live reloading scripts in _addLiveReload when needed - inline: false, - public: serverOptions.publicHost, - allowedHosts: serverOptions.allowedHosts, - disableHostCheck: serverOptions.disableHostCheck, - publicPath: servePath, - hot: serverOptions.hmr, - contentBase: false, - logLevel: 'silent', - }; - if (serverOptions.ssl) { - _addSslConfig(workspaceRoot, serverOptions, config); - } - if (serverOptions.proxyConfig) { - _addProxyConfig(workspaceRoot, serverOptions, config); - } - return config; -} -exports.buildServerConfig = buildServerConfig; -/** - * Resolve and build a URL _path_ that will be the root of the server. This resolved base href and - * deploy URL from the browser options and returns a path from the root. - * @param serverOptions The server options that were passed to the server builder. - * @param browserOptions The browser options that were passed to the browser builder. - * @param logger A generic logger to use for showing warnings. - */ -function buildServePath(serverOptions, browserOptions, logger) { - let servePath = serverOptions.servePath; - if (!servePath && servePath !== '') { - const defaultPath = _findDefaultServePath(browserOptions.baseHref, browserOptions.deployUrl); - const showWarning = serverOptions.servePathDefaultWarning; - if (defaultPath == null && showWarning) { - logger.warn(core_1.tags.oneLine ` - WARNING: --deploy-url and/or --base-href contain unsupported values for ng serve. Default - serve path of '/' used. Use --serve-path to override. - `); - } - servePath = defaultPath || ''; - } - if (servePath.endsWith('/')) { - servePath = servePath.substr(0, servePath.length - 1); - } - if (!servePath.startsWith('/')) { - servePath = `/${servePath}`; - } - return servePath; -} -exports.buildServePath = buildServePath; -/** - * Private method to enhance a webpack config with live reload configuration. - * @private - */ -function _addLiveReload(options, browserOptions, webpackConfig, clientAddress, logger) { - if (webpackConfig.plugins === undefined) { - webpackConfig.plugins = []; - } - // Workaround node shim hoisting issues with live reload client - // Only needed in dev server mode to support live reload capabilities in all package managers - const webpackPath = path.dirname(require.resolve('webpack/package.json')); - const nodeLibsBrowserPath = require.resolve('node-libs-browser', { paths: [webpackPath] }); - const nodeLibsBrowser = require(nodeLibsBrowserPath); - webpackConfig.plugins.push(new webpack.NormalModuleReplacementPlugin(/^events|url|querystring$/, (resource) => { - if (!resource.issuer) { - return; - } - if (/[\/\\]hot[\/\\]emitter\.js$/.test(resource.issuer)) { - if (resource.request === 'events') { - resource.request = nodeLibsBrowser.events; - } - } - else if (/[\/\\]webpack-dev-server[\/\\]client[\/\\]utils[\/\\]createSocketUrl\.js$/.test(resource.issuer)) { - switch (resource.request) { - case 'url': - resource.request = nodeLibsBrowser.url; - break; - case 'querystring': - resource.request = nodeLibsBrowser.querystring; - break; - } - } - })); - // This allows for live reload of page when changes are made to repo. - // https://webpack.js.org/configuration/dev-server/#devserver-inline - let webpackDevServerPath; - try { - webpackDevServerPath = require.resolve('webpack-dev-server/client'); - } - catch (_a) { - throw new Error('The "webpack-dev-server" package could not be found.'); - } - // If a custom path is provided the webpack dev server client drops the sockjs-node segment. - // This adds it back so that behavior is consistent when using a custom URL path - let sockjsPath = ''; - if (clientAddress.pathname) { - clientAddress.pathname = path.posix.join(clientAddress.pathname, 'sockjs-node'); - sockjsPath = '&sockPath=' + clientAddress.pathname; - } - const entryPoints = [`${webpackDevServerPath}?${url.format(clientAddress)}${sockjsPath}`]; - if (options.hmr) { - const webpackHmrLink = 'https://webpack.js.org/guides/hot-module-replacement'; - logger.warn(core_1.tags.oneLine `NOTICE: Hot Module Replacement (HMR) is enabled for the dev server.`); - const showWarning = options.hmrWarning; - if (showWarning) { - logger.info(core_1.tags.stripIndents ` - The project will still live reload when HMR is enabled, - but to take advantage of HMR additional application code is required' - (not included in an Angular CLI project by default).' - See ${webpackHmrLink} - for information on working with HMR for Webpack.`); - logger.warn(core_1.tags.oneLine `To disable this warning use "hmrWarning: false" under "serve" - options in "angular.json".`); - } - entryPoints.push('webpack/hot/dev-server'); - webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); - if (browserOptions.extractCss) { - logger.warn(core_1.tags.oneLine `NOTICE: (HMR) does not allow for CSS hot reload - when used together with '--extract-css'.`); - } - } - if (typeof webpackConfig.entry !== 'object' || Array.isArray(webpackConfig.entry)) { - webpackConfig.entry = {}; - } - if (!Array.isArray(webpackConfig.entry.main)) { - webpackConfig.entry.main = []; - } - webpackConfig.entry.main.unshift(...entryPoints); -} -/** - * Private method to enhance a webpack config with SSL configuration. - * @private - */ -function _addSslConfig(root, options, config) { - let sslKey = undefined; - let sslCert = undefined; - if (options.sslKey) { - const keyPath = path.resolve(root, options.sslKey); - if (fs_1.existsSync(keyPath)) { - sslKey = fs_1.readFileSync(keyPath, 'utf-8'); - } - } - if (options.sslCert) { - const certPath = path.resolve(root, options.sslCert); - if (fs_1.existsSync(certPath)) { - sslCert = fs_1.readFileSync(certPath, 'utf-8'); - } - } - config.https = true; - if (sslKey != null && sslCert != null) { - config.https = { - key: sslKey, - cert: sslCert, - }; - } -} -/** - * Private method to enhance a webpack config with Proxy configuration. - * @private - */ -function _addProxyConfig(root, options, config) { - let proxyConfig = {}; - const proxyPath = path.resolve(root, options.proxyConfig); - if (fs_1.existsSync(proxyPath)) { - proxyConfig = require(proxyPath); - } - else { - const message = 'Proxy config file ' + proxyPath + ' does not exist.'; - throw new Error(message); - } - config.proxy = proxyConfig; -} -/** - * Find the default server path. We don't want to expose baseHref and deployUrl as arguments, only - * the browser options where needed. This method should stay private (people who want to resolve - * baseHref and deployUrl should use the buildServePath exported function. - * @private - */ -function _findDefaultServePath(baseHref, deployUrl) { - if (!baseHref && !deployUrl) { - return ''; - } - if (/^(\w+:)?\/\//.test(baseHref || '') || /^(\w+:)?\/\//.test(deployUrl || '')) { - // If baseHref or deployUrl is absolute, unsupported by ng serve - return null; - } - // normalize baseHref - // for ng serve the starting base is always `/` so a relative - // and root relative value are identical - const baseHrefParts = (baseHref || '').split('/').filter(part => part !== ''); - if (baseHref && !baseHref.endsWith('/')) { - baseHrefParts.pop(); - } - const normalizedBaseHref = baseHrefParts.length === 0 ? '/' : `/${baseHrefParts.join('/')}/`; - if (deployUrl && deployUrl[0] === '/') { - if (baseHref && baseHref[0] === '/' && normalizedBaseHref !== deployUrl) { - // If baseHref and deployUrl are root relative and not equivalent, unsupported by ng serve - return null; - } - return deployUrl; - } - // Join together baseHref and deployUrl - return `${normalizedBaseHref}${deployUrl || ''}`; -} -exports.default = architect_1.createBuilder(serveWebpackBrowser); diff --git a/src/dev-server/schema.d.ts b/src/dev-server/schema.d.ts deleted file mode 100644 index 0e5b5c9db..000000000 --- a/src/dev-server/schema.d.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Dev Server target options for Build Facade. - */ -export interface Schema { - /** - * Whitelist of hosts that are allowed to access the dev server. - */ - allowedHosts?: string[]; - /** - * Build using Ahead of Time compilation. - */ - aot?: boolean; - /** - * Base url for the application being built. - */ - baseHref?: string; - /** - * Target to serve. - */ - browserTarget: string; - /** - * Use a separate bundle containing code used across multiple bundles. - */ - commonChunk?: boolean; - /** - * URL where files will be deployed. - */ - deployUrl?: string; - /** - * Don't verify connected clients are part of allowed hosts. - */ - disableHostCheck?: boolean; - /** - * Enable hot module replacement. - */ - hmr?: boolean; - /** - * Show a warning when the --hmr option is enabled. - */ - hmrWarning?: boolean; - /** - * Host to listen on. - */ - host?: string; - /** - * Whether to reload the page on change, using live-reload. - */ - liveReload?: boolean; - /** - * Opens the url in default browser. - */ - open?: boolean; - /** - * Enables optimization of the build output. - */ - optimization?: OptimizationUnion; - /** - * Enable and define the file watching poll time period in milliseconds. - */ - poll?: number; - /** - * Port to listen on. - */ - port?: number; - /** - * Log progress to the console while building. - */ - progress?: boolean; - /** - * Proxy configuration file. - */ - proxyConfig?: string; - /** - * The URL that the browser client (or live-reload client, if enabled) should use to connect - * to the development server. Use for a complex dev server setup, such as one with reverse - * proxies. - */ - publicHost?: string; - /** - * The pathname where the app will be served. - */ - servePath?: string; - /** - * Show a warning when deploy-url/base-href use unsupported serve path values. - */ - servePathDefaultWarning?: boolean; - /** - * Output sourcemaps. - */ - sourceMap?: SourceMapUnion; - /** - * Serve using HTTPS. - */ - ssl?: boolean; - /** - * SSL certificate to use for serving HTTPS. - */ - sslCert?: string; - /** - * SSL key to use for serving HTTPS. - */ - sslKey?: string; - /** - * Use a separate bundle containing only vendor libraries. - */ - vendorChunk?: boolean; - /** - * Adds more details to output logging. - */ - verbose?: boolean; - /** - * Rebuild on change. - */ - watch?: boolean; -} -/** - * Enables optimization of the build output. - */ -export declare type OptimizationUnion = boolean | OptimizationClass; -export interface OptimizationClass { - /** - * Enables optimization of the scripts output. - */ - scripts?: boolean; - /** - * Enables optimization of the styles output. - */ - styles?: boolean; -} -/** - * Output sourcemaps. - */ -export declare type SourceMapUnion = boolean | SourceMapClass; -export interface SourceMapClass { - /** - * Output sourcemaps used for error reporting tools. - */ - hidden?: boolean; - /** - * Output sourcemaps for all scripts. - */ - scripts?: boolean; - /** - * Output sourcemaps for all styles. - */ - styles?: boolean; - /** - * Resolve vendor packages sourcemaps. - */ - vendor?: boolean; -} diff --git a/src/dev-server/schema.json b/src/dev-server/schema.json deleted file mode 100644 index 64ec242d2..000000000 --- a/src/dev-server/schema.json +++ /dev/null @@ -1,188 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "title": "Dev Server Target", - "description": "Dev Server target options for Build Facade.", - "type": "object", - "properties": { - "browserTarget": { - "type": "string", - "description": "Target to serve.", - "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" - }, - "port": { - "type": "number", - "description": "Port to listen on.", - "default": 4200 - }, - "host": { - "type": "string", - "description": "Host to listen on.", - "default": "localhost" - }, - "proxyConfig": { - "type": "string", - "description": "Proxy configuration file." - }, - "ssl": { - "type": "boolean", - "description": "Serve using HTTPS.", - "default": false - }, - "sslKey": { - "type": "string", - "description": "SSL key to use for serving HTTPS." - }, - "sslCert": { - "type": "string", - "description": "SSL certificate to use for serving HTTPS." - }, - "open": { - "type": "boolean", - "description": "Opens the url in default browser.", - "default": false, - "alias": "o" - }, - "verbose": { - "type": "boolean", - "description": "Adds more details to output logging." - }, - "liveReload": { - "type": "boolean", - "description": "Whether to reload the page on change, using live-reload.", - "default": true - }, - "publicHost": { - "type": "string", - "description": "The URL that the browser client (or live-reload client, if enabled) should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies." - }, - "allowedHosts": { - "type": "array", - "description": "Whitelist of hosts that are allowed to access the dev server.", - "default": [], - "items": { - "type": "string" - } - }, - "servePath": { - "type": "string", - "description": "The pathname where the app will be served." - }, - "disableHostCheck": { - "type": "boolean", - "description": "Don't verify connected clients are part of allowed hosts.", - "default": false - }, - "hmr": { - "type": "boolean", - "description": "Enable hot module replacement.", - "default": false - }, - "watch": { - "type": "boolean", - "description": "Rebuild on change.", - "default": true - }, - "hmrWarning": { - "type": "boolean", - "description": "Show a warning when the --hmr option is enabled.", - "default": true - }, - "servePathDefaultWarning": { - "type": "boolean", - "description": "Show a warning when deploy-url/base-href use unsupported serve path values.", - "default": true - }, - "optimization": { - "description": "Enables optimization of the build output.", - "x-user-analytics": 16, - "oneOf": [ - { - "type": "object", - "properties": { - "scripts": { - "type": "boolean", - "description": "Enables optimization of the scripts output.", - "default": true - }, - "styles": { - "type": "boolean", - "description": "Enables optimization of the styles output.", - "default": true - } - }, - "additionalProperties": false - }, - { - "type": "boolean" - } - ] - }, - "aot": { - "type": "boolean", - "description": "Build using Ahead of Time compilation.", - "x-user-analytics": 13 - }, - "sourceMap": { - "description": "Output sourcemaps.", - "oneOf": [ - { - "type": "object", - "properties": { - "scripts": { - "type": "boolean", - "description": "Output sourcemaps for all scripts.", - "default": true - }, - "styles": { - "type": "boolean", - "description": "Output sourcemaps for all styles.", - "default": true - }, - "hidden": { - "type": "boolean", - "description": "Output sourcemaps used for error reporting tools.", - "default": false - }, - "vendor": { - "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", - "default": false - } - }, - "additionalProperties": false - }, - { - "type": "boolean" - } - ] - }, - "vendorChunk": { - "type": "boolean", - "description": "Use a separate bundle containing only vendor libraries." - }, - "commonChunk": { - "type": "boolean", - "description": "Use a separate bundle containing code used across multiple bundles." - }, - "baseHref": { - "type": "string", - "description": "Base url for the application being built." - }, - "deployUrl": { - "type": "string", - "description": "URL where files will be deployed." - }, - "progress": { - "type": "boolean", - "description": "Log progress to the console while building." - }, - "poll": { - "type": "number", - "description": "Enable and define the file watching poll time period in milliseconds." - } - }, - "additionalProperties": false, - "required": [ - "browserTarget" - ] -} diff --git a/src/extract-i18n/index.d.ts b/src/extract-i18n/index.d.ts deleted file mode 100644 index ac6070a70..000000000 --- a/src/extract-i18n/index.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { BuilderContext } from '@angular-devkit/architect'; -import { BuildResult } from '@angular-devkit/build-webpack'; -import { JsonObject } from '@angular-devkit/core'; -import { Schema } from './schema'; -export declare type ExtractI18nBuilderOptions = Schema & JsonObject; -export declare function execute(options: ExtractI18nBuilderOptions, context: BuilderContext): Promise; -declare const _default: import("@angular-devkit/architect/src/internal").Builder; -export default _default; diff --git a/src/extract-i18n/index.js b/src/extract-i18n/index.js deleted file mode 100644 index 39a06a02c..000000000 --- a/src/extract-i18n/index.js +++ /dev/null @@ -1,112 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.execute = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const architect_1 = require("@angular-devkit/architect"); -const build_webpack_1 = require("@angular-devkit/build-webpack"); -const path = require("path"); -const webpack = require("webpack"); -const webpack_configs_1 = require("../angular-cli-files/models/webpack-configs"); -const stats_1 = require("../angular-cli-files/utilities/stats"); -const i18n_options_1 = require("../utils/i18n-options"); -const version_1 = require("../utils/version"); -const webpack_browser_config_1 = require("../utils/webpack-browser-config"); -const schema_1 = require("./schema"); -function getI18nOutfile(format) { - switch (format) { - case 'xmb': - return 'messages.xmb'; - case 'xlf': - case 'xlif': - case 'xliff': - case 'xlf2': - case 'xliff2': - return 'messages.xlf'; - default: - throw new Error(`Unsupported format "${format}"`); - } -} -class InMemoryOutputPlugin { - apply(compiler) { - // tslint:disable-next-line:no-any - compiler.outputFileSystem = new webpack.MemoryOutputFileSystem(); - } -} -async function execute(options, context) { - // Check Angular version. - version_1.assertCompatibleAngularVersion(context.workspaceRoot, context.logger); - const browserTarget = architect_1.targetFromTargetString(options.browserTarget); - const browserOptions = await context.validateOptions(await context.getTargetOptions(browserTarget), await context.getBuilderNameForTarget(browserTarget)); - if (options.i18nFormat !== schema_1.Format.Xlf) { - options.format = options.i18nFormat; - } - switch (options.format) { - case schema_1.Format.Xlf: - case schema_1.Format.Xlif: - case schema_1.Format.Xliff: - options.format = schema_1.Format.Xlf; - break; - case schema_1.Format.Xlf2: - case schema_1.Format.Xliff2: - options.format = schema_1.Format.Xlf2; - break; - } - // We need to determine the outFile name so that AngularCompiler can retrieve it. - let outFile = options.outFile || getI18nOutfile(options.format); - if (options.outputPath) { - // AngularCompilerPlugin doesn't support genDir so we have to adjust outFile instead. - outFile = path.join(options.outputPath, outFile); - } - const projectName = context.target && context.target.project; - if (!projectName) { - throw new Error('The builder requires a target.'); - } - // target is verified in the above call - // tslint:disable-next-line: no-non-null-assertion - const metadata = await context.getProjectMetadata(context.target); - const i18n = i18n_options_1.createI18nOptions(metadata); - const { config } = await webpack_browser_config_1.generateBrowserWebpackConfigFromContext({ - ...browserOptions, - optimization: { - scripts: false, - styles: false, - }, - buildOptimizer: false, - i18nLocale: options.i18nLocale || i18n.sourceLocale, - i18nFormat: options.format, - i18nFile: outFile, - aot: true, - progress: options.progress, - assets: [], - scripts: [], - styles: [], - deleteOutputPath: false, - }, context, wco => [ - { plugins: [new InMemoryOutputPlugin()] }, - webpack_configs_1.getCommonConfig(wco), - webpack_configs_1.getAotConfig(wco, true), - webpack_configs_1.getStylesConfig(wco), - webpack_configs_1.getStatsConfig(wco), - ]); - const logging = (stats, config) => { - const json = stats.toJson({ errors: true, warnings: true }); - if (stats.hasWarnings()) { - context.logger.warn(stats_1.statsWarningsToString(json, config.stats)); - } - if (stats.hasErrors()) { - context.logger.error(stats_1.statsErrorsToString(json, config.stats)); - } - }; - return build_webpack_1.runWebpack(config, context, { - logging, - webpackFactory: await Promise.resolve().then(() => require('webpack')), - }).toPromise(); -} -exports.execute = execute; -exports.default = architect_1.createBuilder(execute); diff --git a/src/extract-i18n/schema.d.ts b/src/extract-i18n/schema.d.ts deleted file mode 100644 index f436134ee..000000000 --- a/src/extract-i18n/schema.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Extract i18n target options for Build Facade. - */ -export interface Schema { - /** - * Target to extract from. - */ - browserTarget: string; - /** - * Output format for the generated file. - */ - format?: Format; - /** - * Output format for the generated file. - * @deprecated Use 'format' option instead. - */ - i18nFormat?: Format; - /** - * Specifies the source language of the application. - * @deprecated Use 'i18n' project level sub-option 'sourceLocale' instead. - */ - i18nLocale?: string; - /** - * Name of the file to output. - */ - outFile?: string; - /** - * Path where output will be placed. - */ - outputPath?: string; - /** - * Log progress to the console. - */ - progress?: boolean; -} -/** - * Output format for the generated file. - * - * Output format for the generated file. - * @deprecated Use 'format' option instead. - */ -export declare enum Format { - Xlf = "xlf", - Xlf2 = "xlf2", - Xlif = "xlif", - Xliff = "xliff", - Xliff2 = "xliff2", - Xmb = "xmb" -} diff --git a/src/extract-i18n/schema.js b/src/extract-i18n/schema.js deleted file mode 100644 index f41e2f63f..000000000 --- a/src/extract-i18n/schema.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; -// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE -// CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Format = void 0; -/** - * Output format for the generated file. - * - * Output format for the generated file. - * @deprecated Use 'format' option instead. - */ -var Format; -(function (Format) { - Format["Xlf"] = "xlf"; - Format["Xlf2"] = "xlf2"; - Format["Xlif"] = "xlif"; - Format["Xliff"] = "xliff"; - Format["Xliff2"] = "xliff2"; - Format["Xmb"] = "xmb"; -})(Format = exports.Format || (exports.Format = {})); diff --git a/src/extract-i18n/schema.json b/src/extract-i18n/schema.json deleted file mode 100644 index 2d9d89c17..000000000 --- a/src/extract-i18n/schema.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "title": "Extract i18n Target", - "description": "Extract i18n target options for Build Facade.", - "type": "object", - "properties": { - "browserTarget": { - "type": "string", - "description": "Target to extract from.", - "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" - }, - "format": { - "type": "string", - "description": "Output format for the generated file.", - "default": "xlf", - "enum": [ - "xmb", - "xlf", - "xlif", - "xliff", - "xlf2", - "xliff2" - ] - }, - "i18nFormat": { - "type": "string", - "description": "Output format for the generated file.", - "default": "xlf", - "x-deprecated": "Use 'format' option instead.", - "enum": [ - "xmb", - "xlf", - "xlif", - "xliff", - "xlf2", - "xliff2" - ] - }, - "i18nLocale": { - "type": "string", - "description": "Specifies the source language of the application.", - "x-deprecated": "Use 'i18n' project level sub-option 'sourceLocale' instead." - }, - "progress": { - "type": "boolean", - "description": "Log progress to the console.", - "default": true - }, - "outputPath": { - "type": "string", - "description": "Path where output will be placed." - }, - "outFile": { - "type": "string", - "description": "Name of the file to output." - } - }, - "additionalProperties": false, - "required": [ - "browserTarget" - ] -} diff --git a/src/index.d.ts b/src/index.d.ts index 9fb5863e9..6f50b3da1 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,15 +1,19 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ export * from './transforms'; -export { AssetPattern, AssetPatternClass as AssetPatternObject, Budget, CrossOrigin, ExtraEntryPoint, ExtraEntryPointClass as ExtraEntryPointObject, FileReplacement, OptimizationClass as OptimizationObject, OptimizationUnion, OutputHashing, Schema as BrowserBuilderOptions, SourceMapClass as SourceMapObject, SourceMapUnion, StylePreprocessorOptions, Type, } from './browser/schema'; -export { buildWebpackBrowser as executeBrowserBuilder, BrowserBuilderOutput, } from './browser'; -export { serveWebpackBrowser as executeDevServerBuilder, DevServerBuilderOptions, DevServerBuilderOutput, } from './dev-server'; -export { execute as executeExtractI18nBuilder, ExtractI18nBuilderOptions, } from './extract-i18n'; -export { execute as executeKarmaBuilder, KarmaBuilderOptions, KarmaConfigOptions, } from './karma'; -export { execute as executeProtractorBuilder, ProtractorBuilderOptions, } from './protractor'; -export { execute as executeServerBuilder, ServerBuilderOptions, ServerBuilderOutput, } from './server'; +export { CrossOrigin, OutputHashing, Type } from './builders/browser/schema'; +export type { AssetPattern, AssetPatternClass as AssetPatternObject, Budget, FileReplacement, OptimizationClass as OptimizationObject, OptimizationUnion, Schema as BrowserBuilderOptions, SourceMapClass as SourceMapObject, SourceMapUnion, StylePreprocessorOptions, } from './builders/browser/schema'; +export { buildWebpackBrowser as executeBrowserBuilder, type BrowserBuilderOutput, } from './builders/browser'; +export { buildApplication, type ApplicationBuilderOptions } from '@angular/build'; +export { executeDevServerBuilder, type DevServerBuilderOptions, type DevServerBuilderOutput, } from './builders/dev-server'; +export { execute as executeExtractI18nBuilder, type ExtractI18nBuilderOptions, } from './builders/extract-i18n'; +export { execute as executeKarmaBuilder, type KarmaBuilderOptions, type KarmaConfigOptions, } from './builders/karma'; +export { execute as executeProtractorBuilder, type ProtractorBuilderOptions, } from './builders/protractor'; +export { execute as executeServerBuilder, type ServerBuilderOptions, type ServerBuilderOutput, } from './builders/server'; +export { execute as executeSSRDevServerBuilder, type SSRDevServerBuilderOptions, type SSRDevServerBuilderOutput, } from './builders/ssr-dev-server'; +export { execute as executeNgPackagrBuilder, type NgPackagrBuilderOptions, } from './builders/ng-packagr'; diff --git a/src/index.js b/src/index.js index d76a1ba12..7389e57de 100644 --- a/src/index.js +++ b/src/index.js @@ -1,42 +1,47 @@ "use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); -// TODO: remove this commented AJV require. -// We don't actually require AJV, but there is a bug with NPM and peer dependencies that is -// whose workaround is to depend on AJV. -// See https://github.com/angular/angular-cli/issues/9691#issuecomment-367322703 for details. -// We need to add a require here to satisfy the dependency checker. -// require('ajv'); +exports.executeNgPackagrBuilder = exports.executeSSRDevServerBuilder = exports.executeServerBuilder = exports.executeProtractorBuilder = exports.executeKarmaBuilder = exports.executeExtractI18nBuilder = exports.executeDevServerBuilder = exports.buildApplication = exports.executeBrowserBuilder = exports.Type = exports.OutputHashing = exports.CrossOrigin = void 0; __exportStar(require("./transforms"), exports); -var schema_1 = require("./browser/schema"); +var schema_1 = require("./builders/browser/schema"); Object.defineProperty(exports, "CrossOrigin", { enumerable: true, get: function () { return schema_1.CrossOrigin; } }); Object.defineProperty(exports, "OutputHashing", { enumerable: true, get: function () { return schema_1.OutputHashing; } }); Object.defineProperty(exports, "Type", { enumerable: true, get: function () { return schema_1.Type; } }); -var browser_1 = require("./browser"); +var browser_1 = require("./builders/browser"); Object.defineProperty(exports, "executeBrowserBuilder", { enumerable: true, get: function () { return browser_1.buildWebpackBrowser; } }); -var dev_server_1 = require("./dev-server"); -Object.defineProperty(exports, "executeDevServerBuilder", { enumerable: true, get: function () { return dev_server_1.serveWebpackBrowser; } }); -var extract_i18n_1 = require("./extract-i18n"); +var build_1 = require("@angular/build"); +Object.defineProperty(exports, "buildApplication", { enumerable: true, get: function () { return build_1.buildApplication; } }); +var dev_server_1 = require("./builders/dev-server"); +Object.defineProperty(exports, "executeDevServerBuilder", { enumerable: true, get: function () { return dev_server_1.executeDevServerBuilder; } }); +var extract_i18n_1 = require("./builders/extract-i18n"); Object.defineProperty(exports, "executeExtractI18nBuilder", { enumerable: true, get: function () { return extract_i18n_1.execute; } }); -var karma_1 = require("./karma"); +var karma_1 = require("./builders/karma"); Object.defineProperty(exports, "executeKarmaBuilder", { enumerable: true, get: function () { return karma_1.execute; } }); -var protractor_1 = require("./protractor"); +var protractor_1 = require("./builders/protractor"); Object.defineProperty(exports, "executeProtractorBuilder", { enumerable: true, get: function () { return protractor_1.execute; } }); -var server_1 = require("./server"); +var server_1 = require("./builders/server"); Object.defineProperty(exports, "executeServerBuilder", { enumerable: true, get: function () { return server_1.execute; } }); +var ssr_dev_server_1 = require("./builders/ssr-dev-server"); +Object.defineProperty(exports, "executeSSRDevServerBuilder", { enumerable: true, get: function () { return ssr_dev_server_1.execute; } }); +var ng_packagr_1 = require("./builders/ng-packagr"); +Object.defineProperty(exports, "executeNgPackagrBuilder", { enumerable: true, get: function () { return ng_packagr_1.execute; } }); diff --git a/src/karma/index.d.ts b/src/karma/index.d.ts deleted file mode 100644 index ff5699cc9..000000000 --- a/src/karma/index.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; -import { Observable } from 'rxjs'; -import * as webpack from 'webpack'; -import { ExecutionTransformer } from '../transforms'; -import { Schema as KarmaBuilderOptions } from './schema'; -export declare type KarmaConfigOptions = import('karma').ConfigOptions & { - buildWebpack?: unknown; - configFile?: string; -}; -export declare function execute(options: KarmaBuilderOptions, context: BuilderContext, transforms?: { - webpackConfiguration?: ExecutionTransformer; - karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions; -}): Observable; -export { KarmaBuilderOptions }; -declare const _default: import("@angular-devkit/architect/src/internal").Builder & KarmaBuilderOptions>; -export default _default; diff --git a/src/karma/index.js b/src/karma/index.js deleted file mode 100644 index e8dd58e31..000000000 --- a/src/karma/index.js +++ /dev/null @@ -1,115 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.execute = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const architect_1 = require("@angular-devkit/architect"); -const core_1 = require("@angular-devkit/core"); -const path_1 = require("path"); -const rxjs_1 = require("rxjs"); -const operators_1 = require("rxjs/operators"); -const webpack_configs_1 = require("../angular-cli-files/models/webpack-configs"); -const single_test_transform_1 = require("../angular-cli-files/plugins/single-test-transform"); -const find_tests_1 = require("../angular-cli-files/utilities/find-tests"); -const version_1 = require("../utils/version"); -const webpack_browser_config_1 = require("../utils/webpack-browser-config"); -async function initialize(options, context, webpackConfigurationTransformer) { - const { config } = await webpack_browser_config_1.generateBrowserWebpackConfigFromContext( - // only two properties are missing: - // * `outputPath` which is fixed for tests - // * `budgets` which might be incorrect due to extra dev libs - { ...options, outputPath: '', budgets: undefined }, context, wco => [ - webpack_configs_1.getCommonConfig(wco), - webpack_configs_1.getStylesConfig(wco), - webpack_configs_1.getNonAotConfig(wco), - webpack_configs_1.getTestConfig(wco), - webpack_configs_1.getWorkerConfig(wco), - ]); - // tslint:disable-next-line:no-implicit-dependencies - const karma = await Promise.resolve().then(() => require('karma')); - return [ - karma, - webpackConfigurationTransformer ? await webpackConfigurationTransformer(config) : config, - ]; -} -function execute(options, context, transforms = {}) { - // Check Angular version. - version_1.assertCompatibleAngularVersion(context.workspaceRoot, context.logger); - return rxjs_1.from(initialize(options, context, transforms.webpackConfiguration)).pipe(operators_1.switchMap(([karma, webpackConfig]) => new rxjs_1.Observable(subscriber => { - const karmaOptions = {}; - if (options.watch !== undefined) { - karmaOptions.singleRun = !options.watch; - } - // Convert browsers from a string to an array - if (options.browsers) { - karmaOptions.browsers = options.browsers.split(','); - } - if (options.reporters) { - // Split along commas to make it more natural, and remove empty strings. - const reporters = options.reporters - .reduce((acc, curr) => acc.concat(curr.split(',')), []) - .filter(x => !!x); - if (reporters.length > 0) { - karmaOptions.reporters = reporters; - } - } - // prepend special webpack loader that will transform test.ts - if (webpackConfig && - webpackConfig.module && - options.include && - options.include.length > 0) { - const mainFilePath = core_1.getSystemPath(core_1.join(core_1.normalize(context.workspaceRoot), options.main)); - const files = find_tests_1.findTests(options.include, path_1.dirname(mainFilePath), context.workspaceRoot); - // early exit, no reason to start karma - if (!files.length) { - subscriber.error(`Specified patterns: "${options.include.join(', ')}" did not match any spec files`); - return; - } - webpackConfig.module.rules.unshift({ - test: path => path === mainFilePath, - use: { - // cannot be a simple path as it differs between environments - loader: single_test_transform_1.SingleTestTransformLoader, - options: { - files, - logger: context.logger, - }, - }, - }); - } - // Assign additional karmaConfig options to the local ngapp config - karmaOptions.configFile = path_1.resolve(context.workspaceRoot, options.karmaConfig); - karmaOptions.buildWebpack = { - options, - webpackConfig, - // Pass onto Karma to emit BuildEvents. - successCb: () => subscriber.next({ success: true }), - failureCb: () => subscriber.next({ success: false }), - // Workaround for https://github.com/karma-runner/karma/issues/3154 - // When this workaround is removed, user projects need to be updated to use a Karma - // version that has a fix for this issue. - toJSON: () => { }, - logger: context.logger, - }; - // Complete the observable once the Karma server returns. - const karmaServer = new karma.Server(transforms.karmaOptions ? transforms.karmaOptions(karmaOptions) : karmaOptions, () => subscriber.complete()); - // karma typings incorrectly define start's return value as void - // tslint:disable-next-line:no-use-of-empty-return-value - const karmaStart = karmaServer.start(); - // Cleanup, signal Karma to exit. - return () => { - // Karma only has the `stop` method start with 3.1.1, so we must defensively check. - const karmaServerWithStop = karmaServer; - if (typeof karmaServerWithStop.stop === 'function') { - return karmaStart.then(() => karmaServerWithStop.stop()); - } - }; - })), operators_1.defaultIfEmpty({ success: false })); -} -exports.execute = execute; -exports.default = architect_1.createBuilder(execute); diff --git a/src/protractor/index.js b/src/protractor/index.js deleted file mode 100644 index 87193e662..000000000 --- a/src/protractor/index.js +++ /dev/null @@ -1,137 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.execute = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const architect_1 = require("@angular-devkit/architect"); -const core_1 = require("@angular-devkit/core"); -const path_1 = require("path"); -const url = require("url"); -const utils_1 = require("../utils"); -function runProtractor(root, options) { - const additionalProtractorConfig = { - baseUrl: options.baseUrl, - specs: options.specs && options.specs.length ? options.specs : undefined, - suite: options.suite, - jasmineNodeOpts: { - grep: options.grep, - invertGrep: options.invertGrep, - }, - }; - // TODO: Protractor manages process.exit itself, so this target will allways quit the - // process. To work around this we run it in a subprocess. - // https://github.com/angular/protractor/issues/4160 - return utils_1.runModuleAsObservableFork(root, 'protractor/built/launcher', 'init', [path_1.resolve(root, options.protractorConfig), additionalProtractorConfig]).toPromise(); -} -async function updateWebdriver() { - // The webdriver-manager update command can only be accessed via a deep import. - const webdriverDeepImport = 'webdriver-manager/built/lib/cmds/update'; - let path; - try { - const protractorPath = require.resolve('protractor'); - path = require.resolve(webdriverDeepImport, { paths: [protractorPath] }); - } - catch (error) { - if (error.code !== 'MODULE_NOT_FOUND') { - throw error; - } - } - if (!path) { - throw new Error(core_1.tags.stripIndents ` - Cannot automatically find webdriver-manager to update. - Update webdriver-manager manually and run 'ng e2e --no-webdriver-update' instead. - `); - } - // tslint:disable-next-line:max-line-length no-implicit-dependencies - const webdriverUpdate = await Promise.resolve().then(() => require(path)); - // const webdriverUpdate = await import(path) as typeof import ('webdriver-manager/built/lib/cmds/update'); - // run `webdriver-manager update --standalone false --gecko false --quiet` - // if you change this, update the command comment in prev line - return webdriverUpdate.program.run({ - standalone: false, - gecko: false, - quiet: true, - }); -} -async function execute(options, context) { - // ensure that only one of these options is used - if (options.devServerTarget && options.baseUrl) { - throw new Error(core_1.tags.stripIndents ` - The 'baseUrl' option cannot be used with 'devServerTarget'. - When present, 'devServerTarget' will be used to automatically setup 'baseUrl' for Protractor. - `); - } - if (options.webdriverUpdate) { - await updateWebdriver(); - } - let baseUrl = options.baseUrl; - let server; - if (options.devServerTarget) { - const target = architect_1.targetFromTargetString(options.devServerTarget); - const serverOptions = await context.getTargetOptions(target); - const overrides = { watch: false }; - if (options.host !== undefined) { - overrides.host = options.host; - } - else if (typeof serverOptions.host === 'string') { - options.host = serverOptions.host; - } - else { - options.host = overrides.host = 'localhost'; - } - if (options.port !== undefined) { - overrides.port = options.port; - } - else if (typeof serverOptions.port === 'number') { - options.port = serverOptions.port; - } - server = await context.scheduleTarget(target, overrides); - const result = await server.result; - if (!result.success) { - return { success: false }; - } - if (typeof serverOptions.publicHost === 'string') { - let publicHost = serverOptions.publicHost; - if (!/^\w+:\/\//.test(publicHost)) { - publicHost = `${serverOptions.ssl - ? 'https' - : 'http'}://${publicHost}`; - } - const clientUrl = url.parse(publicHost); - baseUrl = url.format(clientUrl); - } - else if (typeof result.baseUrl === 'string') { - baseUrl = result.baseUrl; - } - else if (typeof result.port === 'number') { - baseUrl = url.format({ - protocol: serverOptions.ssl ? 'https' : 'http', - hostname: options.host, - port: result.port.toString(), - }); - } - } - // Like the baseUrl in protractor config file when using the API we need to add - // a trailing slash when provide to the baseUrl. - if (baseUrl && !baseUrl.endsWith('/')) { - baseUrl += '/'; - } - try { - return await runProtractor(context.workspaceRoot, { ...options, baseUrl }); - } - catch (_a) { - return { success: false }; - } - finally { - if (server) { - await server.stop(); - } - } -} -exports.execute = execute; -exports.default = architect_1.createBuilder(execute); diff --git a/src/server/index.d.ts b/src/server/index.d.ts deleted file mode 100644 index d5046b435..000000000 --- a/src/server/index.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; -import { json } from '@angular-devkit/core'; -import { Observable } from 'rxjs'; -import * as webpack from 'webpack'; -import { ExecutionTransformer } from '../transforms'; -import { Schema as ServerBuilderOptions } from './schema'; -export declare type ServerBuilderOutput = json.JsonObject & BuilderOutput & { - baseOutputPath: string; - outputPaths: string[]; - /** - * @deprecated in version 9. Use 'outputPaths' instead. - */ - outputPath: string; -}; -export { ServerBuilderOptions }; -export declare function execute(options: ServerBuilderOptions, context: BuilderContext, transforms?: { - webpackConfiguration?: ExecutionTransformer; -}): Observable; -declare const _default: import("@angular-devkit/architect/src/internal").Builder; -export default _default; diff --git a/src/server/index.js b/src/server/index.js deleted file mode 100644 index c762b5f0b..000000000 --- a/src/server/index.js +++ /dev/null @@ -1,103 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.execute = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const architect_1 = require("@angular-devkit/architect"); -const build_webpack_1 = require("@angular-devkit/build-webpack"); -const core_1 = require("@angular-devkit/core"); -const path = require("path"); -const rxjs_1 = require("rxjs"); -const operators_1 = require("rxjs/operators"); -const typescript_1 = require("typescript"); -const webpack_configs_1 = require("../angular-cli-files/models/webpack-configs"); -const read_tsconfig_1 = require("../angular-cli-files/utilities/read-tsconfig"); -const utils_1 = require("../utils"); -const i18n_inlining_1 = require("../utils/i18n-inlining"); -const output_paths_1 = require("../utils/output-paths"); -const version_1 = require("../utils/version"); -const webpack_browser_config_1 = require("../utils/webpack-browser-config"); -function execute(options, context, transforms = {}) { - const root = context.workspaceRoot; - // Check Angular version. - version_1.assertCompatibleAngularVersion(root, context.logger); - const tsConfig = read_tsconfig_1.readTsconfig(options.tsConfig, root); - const target = tsConfig.options.target || typescript_1.ScriptTarget.ES5; - const baseOutputPath = path.resolve(root, options.outputPath); - let outputPaths; - if (typeof options.bundleDependencies === 'string') { - options.bundleDependencies = options.bundleDependencies === 'all'; - context.logger.warn(`Option 'bundleDependencies' string value is deprecated since version 9. Use a boolean value instead.`); - } - if (!options.bundleDependencies && tsConfig.options.enableIvy) { - // tslint:disable-next-line: no-implicit-dependencies - const { __processed_by_ivy_ngcc__, main = '' } = require('@angular/core/package.json'); - if (!__processed_by_ivy_ngcc__ || - !__processed_by_ivy_ngcc__.main || - main.includes('__ivy_ngcc__')) { - context.logger.warn(core_1.tags.stripIndent ` - WARNING: Turning off 'bundleDependencies' with Ivy may result in undefined behaviour - unless 'node_modules' are transformed using the standalone Angular compatibility compiler (NGCC). - See: http://v9.angular.io/guide/ivy#ivy-and-universal-app-shell - `); - } - } - return rxjs_1.from(initialize(options, context, transforms.webpackConfiguration)).pipe(operators_1.concatMap(({ config, i18n }) => { - return build_webpack_1.runWebpack(config, context, { - webpackFactory: require('webpack'), - }).pipe(operators_1.concatMap(async (output) => { - const { emittedFiles = [], webpackStats } = output; - if (!output.success || !i18n.shouldInline) { - return output; - } - if (!webpackStats) { - throw new Error('Webpack stats build result is required.'); - } - outputPaths = output_paths_1.ensureOutputPaths(baseOutputPath, i18n); - const success = await i18n_inlining_1.i18nInlineEmittedFiles(context, emittedFiles, i18n, baseOutputPath, Array.from(outputPaths.values()), [], - // tslint:disable-next-line: no-non-null-assertion - webpackStats.outputPath, target <= typescript_1.ScriptTarget.ES5, options.i18nMissingTranslation); - return { output, success }; - })); - }), operators_1.map(output => { - if (!output.success) { - return output; - } - return { - ...output, - baseOutputPath, - outputPath: baseOutputPath, - outputPaths: outputPaths || [baseOutputPath], - }; - })); -} -exports.execute = execute; -exports.default = architect_1.createBuilder(execute); -async function initialize(options, context, webpackConfigurationTransform) { - const originalOutputPath = options.outputPath; - const { config, i18n } = await webpack_browser_config_1.generateI18nBrowserWebpackConfigFromContext({ - ...options, - buildOptimizer: false, - aot: true, - platform: 'server', - }, context, wco => [ - webpack_configs_1.getCommonConfig(wco), - webpack_configs_1.getServerConfig(wco), - webpack_configs_1.getStylesConfig(wco), - webpack_configs_1.getStatsConfig(wco), - webpack_configs_1.getAotConfig(wco), - ]); - let transformedConfig; - if (webpackConfigurationTransform) { - transformedConfig = await webpackConfigurationTransform(config); - } - if (options.deleteOutputPath) { - utils_1.deleteOutputDir(context.workspaceRoot, originalOutputPath); - } - return { config: transformedConfig || config, i18n }; -} diff --git a/src/server/schema.d.ts b/src/server/schema.d.ts deleted file mode 100644 index dc18b6435..000000000 --- a/src/server/schema.d.ts +++ /dev/null @@ -1,203 +0,0 @@ -export interface Schema { - /** - * Which external dependencies to bundle into the bundle. By default, all of node_modules - * will be bundled. - */ - bundleDependencies?: BundleDependenciesUnion; - /** - * Delete the output path before building. - */ - deleteOutputPath?: boolean; - /** - * URL where files will be deployed. - */ - deployUrl?: string; - /** - * Exclude the listed external dependencies from being bundled into the bundle. Instead, the - * created bundle relies on these dependencies to be available during runtime. - */ - externalDependencies?: string[]; - /** - * Extract all licenses in a separate file, in the case of production builds only. - */ - extractLicenses?: boolean; - /** - * Replace files with other files in the build. - */ - fileReplacements?: FileReplacement[]; - /** - * Run the TypeScript type checker in a forked process. - */ - forkTypeChecker?: boolean; - /** - * Localization file to use for i18n. - * @deprecated Use 'locales' object in the project metadata instead. - */ - i18nFile?: string; - /** - * Format of the localization file specified with --i18n-file. - * @deprecated No longer needed as the format will be determined automatically. - */ - i18nFormat?: string; - /** - * Locale to use for i18n. - * @deprecated Use 'localize' instead. - */ - i18nLocale?: string; - /** - * How to handle missing translations for i18n. - */ - i18nMissingTranslation?: I18NMissingTranslation; - /** - * List of additional NgModule files that will be lazy loaded. Lazy router modules will be - * discovered automatically. - * @deprecated 'SystemJsNgModuleLoader' is deprecated, and this is part of its usage. Use - * 'import()' syntax instead. - */ - lazyModules?: string[]; - localize?: Localize; - /** - * The name of the main entry-point file. - */ - main: string; - /** - * Use file name for lazy loaded chunks. - */ - namedChunks?: boolean; - /** - * Enables optimization of the build output. - */ - optimization?: OptimizationUnion; - /** - * Define the output filename cache-busting hashing mode. - */ - outputHashing?: OutputHashing; - /** - * Path where output will be placed. - */ - outputPath: string; - /** - * Enable and define the file watching poll time period in milliseconds. - */ - poll?: number; - /** - * Do not use the real path when resolving modules. If unset then will default to `true` if - * NodeJS option --preserve-symlinks is set. - */ - preserveSymlinks?: boolean; - /** - * Log progress to the console while building. - */ - progress?: boolean; - /** - * The path where style resources will be placed, relative to outputPath. - */ - resourcesOutputPath?: string; - /** - * Show circular dependency warnings on builds. - */ - showCircularDependencies?: boolean; - /** - * Output sourcemaps. - */ - sourceMap?: SourceMapUnion; - /** - * Generates a 'stats.json' file which can be analyzed using tools such as - * 'webpack-bundle-analyzer'. - */ - statsJson?: boolean; - /** - * Options to pass to style preprocessors - */ - stylePreprocessorOptions?: StylePreprocessorOptions; - /** - * The name of the TypeScript configuration file. - */ - tsConfig: string; - /** - * Adds more details to output logging. - */ - verbose?: boolean; - /** - * Run build when files change. - */ - watch?: boolean; -} -/** - * Which external dependencies to bundle into the bundle. By default, all of node_modules - * will be bundled. - */ -export declare type BundleDependenciesUnion = boolean | BundleDependenciesEnum; -export declare enum BundleDependenciesEnum { - All = "all", - None = "none" -} -export interface FileReplacement { - replace?: string; - replaceWith?: string; - src?: string; - with?: string; -} -/** - * How to handle missing translations for i18n. - */ -export declare enum I18NMissingTranslation { - Error = "error", - Ignore = "ignore", - Warning = "warning" -} -export declare type Localize = string[] | boolean; -/** - * Enables optimization of the build output. - */ -export declare type OptimizationUnion = boolean | OptimizationClass; -export interface OptimizationClass { - /** - * Enables optimization of the scripts output. - */ - scripts?: boolean; - /** - * Enables optimization of the styles output. - */ - styles?: boolean; -} -/** - * Define the output filename cache-busting hashing mode. - */ -export declare enum OutputHashing { - All = "all", - Bundles = "bundles", - Media = "media", - None = "none" -} -/** - * Output sourcemaps. - */ -export declare type SourceMapUnion = boolean | SourceMapClass; -export interface SourceMapClass { - /** - * Output sourcemaps used for error reporting tools. - */ - hidden?: boolean; - /** - * Output sourcemaps for all scripts. - */ - scripts?: boolean; - /** - * Output sourcemaps for all styles. - */ - styles?: boolean; - /** - * Resolve vendor packages sourcemaps. - */ - vendor?: boolean; -} -/** - * Options to pass to style preprocessors - */ -export interface StylePreprocessorOptions { - /** - * Paths to include. Paths will be resolved to project root. - */ - includePaths?: string[]; -} diff --git a/src/server/schema.js b/src/server/schema.js deleted file mode 100644 index f9f3d05c4..000000000 --- a/src/server/schema.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE -// CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). -Object.defineProperty(exports, "__esModule", { value: true }); -exports.OutputHashing = exports.I18NMissingTranslation = exports.BundleDependenciesEnum = void 0; -var BundleDependenciesEnum; -(function (BundleDependenciesEnum) { - BundleDependenciesEnum["All"] = "all"; - BundleDependenciesEnum["None"] = "none"; -})(BundleDependenciesEnum = exports.BundleDependenciesEnum || (exports.BundleDependenciesEnum = {})); -/** - * How to handle missing translations for i18n. - */ -var I18NMissingTranslation; -(function (I18NMissingTranslation) { - I18NMissingTranslation["Error"] = "error"; - I18NMissingTranslation["Ignore"] = "ignore"; - I18NMissingTranslation["Warning"] = "warning"; -})(I18NMissingTranslation = exports.I18NMissingTranslation || (exports.I18NMissingTranslation = {})); -/** - * Define the output filename cache-busting hashing mode. - */ -var OutputHashing; -(function (OutputHashing) { - OutputHashing["All"] = "all"; - OutputHashing["Bundles"] = "bundles"; - OutputHashing["Media"] = "media"; - OutputHashing["None"] = "none"; -})(OutputHashing = exports.OutputHashing || (exports.OutputHashing = {})); diff --git a/src/test-utils.d.ts b/src/test-utils.d.ts deleted file mode 100644 index bd32e0264..000000000 --- a/src/test-utils.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Architect, BuilderOutput, ScheduleOptions, Target } from '@angular-devkit/architect'; -import { TestProjectHost, TestingArchitectHost } from '@angular-devkit/architect/testing'; -import { Path, json, virtualFs, workspaces } from '@angular-devkit/core'; -export declare let veEnabled: boolean; -export declare const workspaceRoot: Path; -export declare const host: TestProjectHost; -export declare const outputPath: Path; -export declare const browserTargetSpec: { - project: string; - target: string; -}; -export declare const devServerTargetSpec: { - project: string; - target: string; -}; -export declare const extractI18nTargetSpec: { - project: string; - target: string; -}; -export declare const karmaTargetSpec: { - project: string; - target: string; -}; -export declare const tslintTargetSpec: { - project: string; - target: string; -}; -export declare const protractorTargetSpec: { - project: string; - target: string; -}; -export declare function createArchitect(workspaceRoot: Path): Promise<{ - workspace: workspaces.WorkspaceDefinition; - architectHost: TestingArchitectHost; - architect: Architect; -}>; -export interface BrowserBuildOutput { - output: BuilderOutput; - files: { - [file: string]: Promise; - }; -} -export declare function browserBuild(architect: Architect, host: virtualFs.Host, target: Target, overrides?: json.JsonObject, scheduleOptions?: ScheduleOptions): Promise; -export declare const lazyModuleFiles: { - [path: string]: string; -}; -export declare const lazyModuleStringImport: { - [path: string]: string; -}; -export declare const lazyModuleFnImport: { - [path: string]: string; -}; diff --git a/src/test-utils.js b/src/test-utils.js deleted file mode 100644 index 00bf3a7cd..000000000 --- a/src/test-utils.js +++ /dev/null @@ -1,135 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.lazyModuleFnImport = exports.lazyModuleStringImport = exports.lazyModuleFiles = exports.browserBuild = exports.createArchitect = exports.protractorTargetSpec = exports.tslintTargetSpec = exports.karmaTargetSpec = exports.extractI18nTargetSpec = exports.devServerTargetSpec = exports.browserTargetSpec = exports.outputPath = exports.host = exports.workspaceRoot = exports.veEnabled = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const architect_1 = require("@angular-devkit/architect"); -const node_1 = require("@angular-devkit/architect/node"); -const testing_1 = require("@angular-devkit/architect/testing"); -const core_1 = require("@angular-devkit/core"); -// Default timeout for large specs is 2.5 minutes. -jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000; -// This flag controls whether AOT compilation uses Ivy or View Engine (VE). -exports.veEnabled = process.argv.some(arg => arg == 'view_engine'); -exports.workspaceRoot = core_1.join(core_1.normalize(__dirname), `../test/hello-world-app/`); -exports.host = new testing_1.TestProjectHost(exports.workspaceRoot); -exports.outputPath = core_1.normalize('dist'); -exports.browserTargetSpec = { project: 'app', target: 'build' }; -exports.devServerTargetSpec = { project: 'app', target: 'serve' }; -exports.extractI18nTargetSpec = { project: 'app', target: 'extract-i18n' }; -exports.karmaTargetSpec = { project: 'app', target: 'test' }; -exports.tslintTargetSpec = { project: 'app', target: 'lint' }; -exports.protractorTargetSpec = { project: 'app-e2e', target: 'e2e' }; -async function createArchitect(workspaceRoot) { - const registry = new core_1.schema.CoreSchemaRegistry(); - registry.addPostTransform(core_1.schema.transforms.addUndefinedDefaults); - const workspaceSysPath = core_1.getSystemPath(workspaceRoot); - const { workspace } = await core_1.workspaces.readWorkspace(workspaceSysPath, core_1.workspaces.createWorkspaceHost(exports.host)); - const architectHost = new testing_1.TestingArchitectHost(workspaceSysPath, workspaceSysPath, new node_1.WorkspaceNodeModulesArchitectHost(workspace, workspaceSysPath)); - const architect = new architect_1.Architect(architectHost, registry); - // Set AOT compilation to use VE if needed. - if (exports.veEnabled) { - exports.host.replaceInFile('tsconfig.base.json', `"enableIvy": true,`, `"enableIvy": false,`); - } - return { - workspace, - architectHost, - architect, - }; -} -exports.createArchitect = createArchitect; -async function browserBuild(architect, host, target, overrides, scheduleOptions) { - const run = await architect.scheduleTarget(target, overrides, scheduleOptions); - const output = (await run.result); - expect(output.success).toBe(true); - expect(output.outputPaths[0]).not.toBeUndefined(); - const outputPath = core_1.normalize(output.outputPaths[0]); - const fileNames = await host.list(outputPath).toPromise(); - const files = fileNames.reduce((acc, path) => { - let cache = null; - Object.defineProperty(acc, path, { - enumerable: true, - get() { - if (cache) { - return cache; - } - if (!fileNames.includes(path)) { - return Promise.reject('No file named ' + path); - } - cache = host - .read(core_1.join(outputPath, path)) - .toPromise() - .then(content => core_1.virtualFs.fileBufferToString(content)); - return cache; - }, - }); - return acc; - }, {}); - await run.stop(); - return { - output, - files, - }; -} -exports.browserBuild = browserBuild; -exports.lazyModuleFiles = { - 'src/app/lazy/lazy-routing.module.ts': ` - import { NgModule } from '@angular/core'; - import { Routes, RouterModule } from '@angular/router'; - - const routes: Routes = []; - - @NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] - }) - export class LazyRoutingModule { } - `, - 'src/app/lazy/lazy.module.ts': ` - import { NgModule } from '@angular/core'; - import { CommonModule } from '@angular/common'; - - import { LazyRoutingModule } from './lazy-routing.module'; - - @NgModule({ - imports: [ - CommonModule, - LazyRoutingModule - ], - declarations: [] - }) - export class LazyModule { } - `, -}; -exports.lazyModuleStringImport = { - 'src/app/app.module.ts': ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - - import { AppComponent } from './app.component'; - import { RouterModule } from '@angular/router'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - RouterModule.forRoot([ - { path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule' } - ]) - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - `, -}; -exports.lazyModuleFnImport = { - 'src/app/app.module.ts': exports.lazyModuleStringImport['src/app/app.module.ts'].replace(`loadChildren: './lazy/lazy.module#LazyModule'`, `loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)`), -}; diff --git a/src/tools/babel/babel-loader.d.ts b/src/tools/babel/babel-loader.d.ts new file mode 100644 index 000000000..a3d8f938a --- /dev/null +++ b/src/tools/babel/babel-loader.d.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +declare module 'babel-loader' { + type BabelLoaderCustomizer = (babel: typeof import('@babel/core')) => { + customOptions?( + this: import('webpack').loader.LoaderContext, + loaderOptions: Record, + loaderArguments: { source: string; map?: unknown }, + ): Promise<{ custom?: T; loader: Record }>; + config?( + this: import('webpack').loader.LoaderContext, + configuration: import('@babel/core').PartialConfig, + loaderArguments: { source: string; map?: unknown; customOptions: T }, + ): import('@babel/core').TransformOptions; + result?( + this: import('webpack').loader.LoaderContext, + result: import('@babel/core').BabelFileResult, + context: { + source: string; + map?: unknown; + customOptions: T; + configuration: import('@babel/core').PartialConfig; + options: import('@babel/core').TransformOptions; + }, + ): import('@babel/core').BabelFileResult; + }; + function custom(customizer: BabelLoaderCustomizer): import('webpack').loader.Loader; +} diff --git a/src/tools/babel/plugins/add-code-coverage.d.ts b/src/tools/babel/plugins/add-code-coverage.d.ts new file mode 100644 index 000000000..e91a8e4d7 --- /dev/null +++ b/src/tools/babel/plugins/add-code-coverage.d.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { PluginObj } from '@babel/core'; +/** + * A babel plugin factory function for adding istanbul instrumentation. + * + * @returns A babel plugin object instance. + */ +export default function (): PluginObj; diff --git a/src/tools/babel/plugins/add-code-coverage.js b/src/tools/babel/plugins/add-code-coverage.js new file mode 100644 index 000000000..6cb4cdf3d --- /dev/null +++ b/src/tools/babel/plugins/add-code-coverage.js @@ -0,0 +1,44 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +const core_1 = require("@babel/core"); +const istanbul_lib_instrument_1 = require("istanbul-lib-instrument"); +const node_assert_1 = __importDefault(require("node:assert")); +/** + * A babel plugin factory function for adding istanbul instrumentation. + * + * @returns A babel plugin object instance. + */ +function default_1() { + const visitors = new WeakMap(); + return { + visitor: { + Program: { + enter(path, state) { + const visitor = (0, istanbul_lib_instrument_1.programVisitor)(core_1.types, state.filename, { + // Babel returns a Converter object from the `convert-source-map` package + inputSourceMap: state.file.inputMap?.toObject(), + }); + visitors.set(path, visitor); + visitor.enter(path); + }, + exit(path) { + const visitor = visitors.get(path); + (0, node_assert_1.default)(visitor, 'Instrumentation visitor should always be present for program path.'); + visitor.exit(path); + visitors.delete(path); + }, + }, + }, + }; +} diff --git a/src/tools/babel/plugins/types.d.ts b/src/tools/babel/plugins/types.d.ts new file mode 100644 index 000000000..4ff052dcb --- /dev/null +++ b/src/tools/babel/plugins/types.d.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +declare module 'istanbul-lib-instrument' { + export interface Visitor { + enter(path: import('@babel/core').NodePath): void; + exit(path: import('@babel/core').NodePath): void; + } + + export function programVisitor( + types: typeof import('@babel/core').types, + filePath?: string, + options?: { inputSourceMap?: object | null }, + ): Visitor; +} diff --git a/src/tools/babel/presets/application.d.ts b/src/tools/babel/presets/application.d.ts new file mode 100644 index 000000000..d6ed49e5f --- /dev/null +++ b/src/tools/babel/presets/application.d.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { ɵParsedTranslation } from '@angular/localize'; +import type { makeEs2015TranslatePlugin, makeLocalePlugin } from '@angular/localize/tools'; +export type DiagnosticReporter = (type: 'error' | 'warning' | 'info', message: string) => void; +/** + * An interface representing the factory functions for the `@angular/localize` translation Babel plugins. + * This must be provided for the ESM imports since dynamic imports are required to be asynchronous and + * Babel presets currently can only be synchronous. + * + */ +export interface I18nPluginCreators { + makeEs2015TranslatePlugin: typeof makeEs2015TranslatePlugin; + makeLocalePlugin: typeof makeLocalePlugin; +} +export interface ApplicationPresetOptions { + i18n?: { + locale: string; + missingTranslationBehavior?: 'error' | 'warning' | 'ignore'; + translation?: Record; + translationFiles?: string[]; + pluginCreators: I18nPluginCreators; + }; + angularLinker?: { + shouldLink: boolean; + jitMode: boolean; + linkerPluginCreator: typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin; + }; + forceAsyncTransformation?: boolean; + instrumentCode?: { + includedBasePath: string; + inputSourceMap: unknown; + }; + optimize?: { + topLevelSafeMode: boolean; + wrapDecorators: boolean; + }; + supportedBrowsers?: string[]; + diagnosticReporter?: DiagnosticReporter; +} +export default function (api: unknown, options: ApplicationPresetOptions): { + presets: any[][]; + plugins: any[]; +}; +export declare function requiresLinking(path: string, source: string): Promise; diff --git a/src/tools/babel/presets/application.js b/src/tools/babel/presets/application.js new file mode 100644 index 000000000..60639226b --- /dev/null +++ b/src/tools/babel/presets/application.js @@ -0,0 +1,159 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +exports.requiresLinking = requiresLinking; +const node_assert_1 = __importDefault(require("node:assert")); +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +const load_esm_1 = require("../../../utils/load-esm"); +/** + * Cached instance of the compiler-cli linker's needsLinking function. + */ +let needsLinking; +function createI18nDiagnostics(reporter) { + const diagnostics = new (class { + messages = []; + hasErrors = false; + add(type, message) { + if (type === 'ignore') { + return; + } + this.messages.push({ type, message }); + this.hasErrors ||= type === 'error'; + reporter?.(type, message); + } + error(message) { + this.add('error', message); + } + warn(message) { + this.add('warning', message); + } + merge(other) { + for (const diagnostic of other.messages) { + this.add(diagnostic.type, diagnostic.message); + } + } + formatDiagnostics() { + node_assert_1.default.fail('@angular/localize Diagnostics formatDiagnostics should not be called from within babel.'); + } + })(); + return diagnostics; +} +function createI18nPlugins(locale, translation, missingTranslationBehavior, diagnosticReporter, pluginCreators) { + const diagnostics = createI18nDiagnostics(diagnosticReporter); + const plugins = []; + const { makeEs2015TranslatePlugin, makeLocalePlugin } = pluginCreators; + if (translation) { + plugins.push(makeEs2015TranslatePlugin(diagnostics, translation, { + missingTranslation: missingTranslationBehavior, + })); + } + plugins.push(makeLocalePlugin(locale)); + return plugins; +} +function createNgtscLogger(reporter) { + return { + level: 1, // Info level + debug(...args) { }, + info(...args) { + reporter?.('info', args.join()); + }, + warn(...args) { + reporter?.('warning', args.join()); + }, + error(...args) { + reporter?.('error', args.join()); + }, + }; +} +function default_1(api, options) { + const presets = []; + const plugins = []; + let needRuntimeTransform = false; + if (options.angularLinker?.shouldLink) { + plugins.push(options.angularLinker.linkerPluginCreator({ + linkerJitMode: options.angularLinker.jitMode, + // This is a workaround until https://github.com/angular/angular/issues/42769 is fixed. + sourceMapping: false, + logger: createNgtscLogger(options.diagnosticReporter), + fileSystem: { + resolve: node_path_1.default.resolve, + exists: node_fs_1.default.existsSync, + dirname: node_path_1.default.dirname, + relative: node_path_1.default.relative, + readFile: node_fs_1.default.readFileSync, + // Node.JS types don't overlap the Compiler types. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }, + })); + } + // Applications code ES version can be controlled using TypeScript's `target` option. + // However, this doesn't effect libraries and hence we use preset-env to downlevel ES features + // based on the supported browsers in browserslist. + if (options.supportedBrowsers) { + presets.push([ + require('@babel/preset-env').default, + { + bugfixes: true, + modules: false, + targets: options.supportedBrowsers, + exclude: ['transform-typeof-symbol'], + }, + ]); + needRuntimeTransform = true; + } + if (options.i18n) { + const { locale, missingTranslationBehavior, pluginCreators, translation } = options.i18n; + const i18nPlugins = createI18nPlugins(locale, translation, missingTranslationBehavior || 'ignore', options.diagnosticReporter, pluginCreators); + plugins.push(...i18nPlugins); + } + if (options.forceAsyncTransformation) { + // Always transform async/await to support Zone.js + plugins.push(require('@babel/plugin-transform-async-to-generator').default, require('@babel/plugin-transform-async-generator-functions').default); + needRuntimeTransform = true; + } + if (options.optimize) { + const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure, } = require('@angular/build/private'); + plugins.push([markTopLevelPure, { topLevelSafeMode: options.optimize.topLevelSafeMode }], elideAngularMetadata, adjustTypeScriptEnums, [adjustStaticMembers, { wrapDecorators: options.optimize.wrapDecorators }]); + } + if (options.instrumentCode) { + plugins.push(require('../plugins/add-code-coverage').default); + } + if (needRuntimeTransform) { + // Babel equivalent to TypeScript's `importHelpers` option + plugins.push([ + require('@babel/plugin-transform-runtime').default, + { + useESModules: true, + version: require('@babel/runtime/package.json').version, + absoluteRuntime: node_path_1.default.dirname(require.resolve('@babel/runtime/package.json')), + }, + ]); + } + return { presets, plugins }; +} +async function requiresLinking(path, source) { + // @angular/core and @angular/compiler will cause false positives + // Also, TypeScript files do not require linking + if (/[\\/]@angular[\\/](?:compiler|core)|\.tsx?$/.test(path)) { + return false; + } + if (!needsLinking) { + // Load ESM `@angular/compiler-cli/linker` using the TypeScript dynamic import workaround. + // Once TypeScript provides support for keeping the dynamic import this workaround can be + // changed to a direct dynamic import. + const linkerModule = await (0, load_esm_1.loadEsmModule)('@angular/compiler-cli/linker'); + needsLinking = linkerModule.needsLinking; + } + return needsLinking(path, source); +} diff --git a/src/tools/babel/webpack-loader.d.ts b/src/tools/babel/webpack-loader.d.ts new file mode 100644 index 000000000..a9e66e6e7 --- /dev/null +++ b/src/tools/babel/webpack-loader.d.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { ApplicationPresetOptions } from './presets/application'; +interface AngularCustomOptions extends Omit { + instrumentCode?: { + /** node_modules and test files are always excluded. */ + excludedPaths: Set; + includedBasePath: string; + }; +} +export type AngularBabelLoaderOptions = AngularCustomOptions & Record; +declare const _default: any; +export default _default; diff --git a/src/tools/babel/webpack-loader.js b/src/tools/babel/webpack-loader.js new file mode 100644 index 000000000..0dd256fbf --- /dev/null +++ b/src/tools/babel/webpack-loader.js @@ -0,0 +1,168 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const babel_loader_1 = require("babel-loader"); +const load_esm_1 = require("../../utils/load-esm"); +const package_version_1 = require("../../utils/package-version"); +const application_1 = require("./presets/application"); +/** + * Cached instance of the compiler-cli linker's Babel plugin factory function. + */ +let linkerPluginCreator; +/** + * Cached instance of the localize Babel plugins factory functions. + */ +let i18nPluginCreators; +// eslint-disable-next-line max-lines-per-function +exports.default = (0, babel_loader_1.custom)(() => { + const baseOptions = Object.freeze({ + babelrc: false, + configFile: false, + compact: false, + cacheCompression: false, + sourceType: 'unambiguous', + inputSourceMap: false, + }); + return { + async customOptions(options, { source, map }) { + const { i18n, aot, optimize, instrumentCode, supportedBrowsers, ...rawOptions } = options; + // Must process file if plugins are added + let shouldProcess = Array.isArray(rawOptions.plugins) && rawOptions.plugins.length > 0; + const customOptions = { + forceAsyncTransformation: false, + angularLinker: undefined, + i18n: undefined, + instrumentCode: undefined, + supportedBrowsers, + }; + // Analyze file for linking + if (await (0, application_1.requiresLinking)(this.resourcePath, source)) { + // Load ESM `@angular/compiler-cli/linker/babel` using the TypeScript dynamic import workaround. + // Once TypeScript provides support for keeping the dynamic import this workaround can be + // changed to a direct dynamic import. + linkerPluginCreator ??= (await (0, load_esm_1.loadEsmModule)('@angular/compiler-cli/linker/babel')).createEs2015LinkerPlugin; + customOptions.angularLinker = { + shouldLink: true, + jitMode: aot !== true, + linkerPluginCreator, + }; + shouldProcess = true; + } + // Application code (TS files) will only contain native async if target is ES2017+. + // However, third-party libraries can regardless of the target option. + // APF packages with code in [f]esm2015 directories is downlevelled to ES2015 and + // will not have native async. + customOptions.forceAsyncTransformation = + !/[\\/][_f]?esm2015[\\/]/.test(this.resourcePath) && source.includes('async'); + shouldProcess ||= + customOptions.forceAsyncTransformation || + customOptions.supportedBrowsers !== undefined || + false; + // Analyze for i18n inlining + if (i18n && + !/[\\/]@angular[\\/](?:compiler|localize)/.test(this.resourcePath) && + source.includes('$localize')) { + // Load the i18n plugin creators from the new `@angular/localize/tools` entry point. + // This may fail during the transition to ESM due to the entry point not yet existing. + // During the transition, this will always attempt to load the entry point for each file. + // This will only occur during prerelease and will be automatically corrected once the new + // entry point exists. + if (i18nPluginCreators === undefined) { + // Load ESM `@angular/localize/tools` using the TypeScript dynamic import workaround. + // Once TypeScript provides support for keeping the dynamic import this workaround can be + // changed to a direct dynamic import. + i18nPluginCreators = await (0, load_esm_1.loadEsmModule)('@angular/localize/tools'); + } + customOptions.i18n = { + ...i18n, + pluginCreators: i18nPluginCreators, + }; + // Add translation files as dependencies of the file to support rebuilds + // Except for `@angular/core` which needs locale injection but has no translations + if (customOptions.i18n.translationFiles && + !/[\\/]@angular[\\/]core/.test(this.resourcePath)) { + for (const file of customOptions.i18n.translationFiles) { + this.addDependency(file); + } + } + shouldProcess = true; + } + if (optimize) { + const AngularPackage = /[\\/]node_modules[\\/]@angular[\\/]/.test(this.resourcePath); + const sideEffectFree = !!this._module?.factoryMeta?.sideEffectFree; + customOptions.optimize = { + // Angular packages provide additional tested side effects guarantees and can use + // otherwise unsafe optimizations. (@angular/platform-server/init) however has side-effects. + topLevelSafeMode: !(AngularPackage && sideEffectFree), + // JavaScript modules that are marked as side effect free are considered to have + // no decorators that contain non-local effects. + wrapDecorators: sideEffectFree, + }; + shouldProcess = true; + } + if (instrumentCode && + !instrumentCode.excludedPaths.has(this.resourcePath) && + !/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]/.test(this.resourcePath) && + this.resourcePath.startsWith(instrumentCode.includedBasePath)) { + // `babel-plugin-istanbul` has it's own includes but we do the below so that we avoid running the loader. + customOptions.instrumentCode = { + includedBasePath: instrumentCode.includedBasePath, + inputSourceMap: map, + }; + shouldProcess = true; + } + // Add provided loader options to default base options + const loaderOptions = { + ...baseOptions, + ...rawOptions, + cacheIdentifier: JSON.stringify({ + buildAngular: package_version_1.VERSION, + customOptions, + baseOptions, + rawOptions, + }), + }; + // Skip babel processing if no actions are needed + if (!shouldProcess) { + // Force the current file to be ignored + loaderOptions.ignore = [() => true]; + } + return { custom: customOptions, loader: loaderOptions }; + }, + config(configuration, { customOptions }) { + return { + ...configuration.options, + // Using `false` disables babel from attempting to locate sourcemaps or process any inline maps. + // The babel types do not include the false option even though it is valid + // eslint-disable-next-line @typescript-eslint/no-explicit-any + inputSourceMap: configuration.options.inputSourceMap ?? false, + presets: [ + ...(configuration.options.presets || []), + [ + require('./presets/application').default, + { + ...customOptions, + diagnosticReporter: (type, message) => { + switch (type) { + case 'error': + this.emitError(message); + break; + case 'info': // Webpack does not currently have an informational diagnostic + case 'warning': + this.emitWarning(message); + break; + } + }, + }, + ], + ], + }; + }, + }; +}); diff --git a/src/tools/webpack/configs/common.d.ts b/src/tools/webpack/configs/common.d.ts new file mode 100644 index 000000000..85b1547e7 --- /dev/null +++ b/src/tools/webpack/configs/common.d.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Configuration } from 'webpack'; +import { WebpackConfigOptions } from '../../../utils/build-options'; +export declare function getCommonConfig(wco: WebpackConfigOptions): Promise; diff --git a/src/tools/webpack/configs/common.js b/src/tools/webpack/configs/common.js new file mode 100644 index 000000000..8610ed01f --- /dev/null +++ b/src/tools/webpack/configs/common.js @@ -0,0 +1,433 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getCommonConfig = getCommonConfig; +const webpack_1 = require("@ngtools/webpack"); +const copy_webpack_plugin_1 = __importDefault(require("copy-webpack-plugin")); +const path = __importStar(require("node:path")); +const webpack_2 = require("webpack"); +const webpack_subresource_integrity_1 = require("webpack-subresource-integrity"); +const environment_options_1 = require("../../../utils/environment-options"); +const load_esm_1 = require("../../../utils/load-esm"); +const plugins_1 = require("../plugins"); +const devtools_ignore_plugin_1 = require("../plugins/devtools-ignore-plugin"); +const named_chunks_plugin_1 = require("../plugins/named-chunks-plugin"); +const occurrences_plugin_1 = require("../plugins/occurrences-plugin"); +const progress_plugin_1 = require("../plugins/progress-plugin"); +const transfer_size_plugin_1 = require("../plugins/transfer-size-plugin"); +const typescript_1 = require("../plugins/typescript"); +const watch_files_logs_plugin_1 = require("../plugins/watch-files-logs-plugin"); +const helpers_1 = require("../utils/helpers"); +const VENDORS_TEST = /[\\/]node_modules[\\/]/; +// eslint-disable-next-line max-lines-per-function +async function getCommonConfig(wco) { + const { root, projectRoot, buildOptions, tsConfig, projectName, sourceRoot, tsConfigPath } = wco; + const { cache, codeCoverage, crossOrigin = 'none', platform = 'browser', aot = true, codeCoverageExclude = [], main, sourceMap: { styles: stylesSourceMap, scripts: scriptsSourceMap, vendor: vendorSourceMap, hidden: hiddenSourceMap, }, optimization: { styles: stylesOptimization, scripts: scriptsOptimization }, commonChunk, vendorChunk, subresourceIntegrity, verbose, poll, webWorkerTsConfig, externalDependencies = [], allowedCommonJsDependencies, } = buildOptions; + const isPlatformServer = buildOptions.platform === 'server'; + const extraPlugins = []; + const extraRules = []; + const entryPoints = {}; + // Load ESM `@angular/compiler-cli` using the TypeScript dynamic import workaround. + // Once TypeScript provides support for keeping the dynamic import this workaround can be + // changed to a direct dynamic import. + const { VERSION: NG_VERSION } = await (0, load_esm_1.loadEsmModule)('@angular/compiler-cli'); + const { GLOBAL_DEFS_FOR_TERSER, GLOBAL_DEFS_FOR_TERSER_WITH_AOT } = await (0, load_esm_1.loadEsmModule)('@angular/compiler-cli/private/tooling'); + // determine hashing format + const hashFormat = (0, helpers_1.getOutputHashFormat)(buildOptions.outputHashing); + if (buildOptions.progress) { + extraPlugins.push(new progress_plugin_1.ProgressPlugin(platform)); + } + const localizePackageInitEntryPoint = '@angular/localize/init'; + const hasLocalizeType = tsConfig.options.types?.some((t) => t === '@angular/localize' || t === localizePackageInitEntryPoint); + if (hasLocalizeType) { + entryPoints['main'] = [localizePackageInitEntryPoint]; + } + if (buildOptions.main) { + const mainPath = path.resolve(root, buildOptions.main); + if (Array.isArray(entryPoints['main'])) { + entryPoints['main'].push(mainPath); + } + else { + entryPoints['main'] = [mainPath]; + } + } + if (isPlatformServer) { + // Fixes Critical dependency: the request of a dependency is an expression + extraPlugins.push(new webpack_2.ContextReplacementPlugin(/@?hapi|express[\\/]/)); + if ((0, helpers_1.isPackageInstalled)(wco.root, '@angular/platform-server') && + Array.isArray(entryPoints['main'])) { + // This import must come before any imports (direct or transitive) that rely on DOM built-ins being + // available, such as `@angular/elements`. + entryPoints['main'].unshift('@angular/platform-server/init'); + } + } + const polyfills = [...buildOptions.polyfills]; + if (!aot) { + polyfills.push('@angular/compiler'); + } + if (polyfills.length) { + // `zone.js/testing` is a **special** polyfill because when not imported in the main it fails with the below errors: + // `Error: Expected to be running in 'ProxyZone', but it was not found.` + // This was also the reason why previously it was imported in `test.ts` as the first module. + // From Jia li: + // This is because the jasmine functions such as beforeEach/it will not be patched by zone.js since + // jasmine will not be loaded yet, so the ProxyZone will not be there. We have to load zone-testing.js after + // jasmine is ready. + // We could force loading 'zone.js/testing' prior to jasmine by changing the order of scripts in 'karma-context.html'. + // But this has it's own problems as zone.js needs to be loaded prior to jasmine due to patching of timing functions + // See: https://github.com/jasmine/jasmine/issues/1944 + // Thus the correct order is zone.js -> jasmine -> zone.js/testing. + const zoneTestingEntryPoint = 'zone.js/testing'; + const polyfillsExludingZoneTesting = polyfills.filter((p) => p !== zoneTestingEntryPoint); + if (Array.isArray(entryPoints['polyfills'])) { + entryPoints['polyfills'].push(...polyfillsExludingZoneTesting); + } + else { + entryPoints['polyfills'] = polyfillsExludingZoneTesting; + } + if (polyfillsExludingZoneTesting.length !== polyfills.length) { + if (Array.isArray(entryPoints['main'])) { + entryPoints['main'].unshift(zoneTestingEntryPoint); + } + else { + entryPoints['main'] = [zoneTestingEntryPoint]; + } + } + } + if (allowedCommonJsDependencies) { + // When this is not defined it means the builder doesn't support showing common js usages. + // When it does it will be an array. + extraPlugins.push(new plugins_1.CommonJsUsageWarnPlugin({ + allowedDependencies: allowedCommonJsDependencies, + })); + } + // process global scripts + // Add a new asset for each entry. + for (const { bundleName, inject, paths } of (0, helpers_1.globalScriptsByBundleName)(buildOptions.scripts)) { + // Lazy scripts don't get a hash, otherwise they can't be loaded by name. + const hash = inject ? hashFormat.script : ''; + extraPlugins.push(new plugins_1.ScriptsWebpackPlugin({ + name: bundleName, + sourceMap: scriptsSourceMap, + scripts: paths, + filename: `${path.basename(bundleName)}${hash}.js`, + basePath: root, + })); + } + // process asset entries + if (buildOptions.assets.length) { + extraPlugins.push(new copy_webpack_plugin_1.default({ + patterns: (0, helpers_1.assetPatterns)(root, buildOptions.assets), + })); + } + if (buildOptions.extractLicenses) { + const LicenseWebpackPlugin = require('license-webpack-plugin').LicenseWebpackPlugin; + extraPlugins.push(new LicenseWebpackPlugin({ + stats: { + warnings: false, + errors: false, + }, + perChunkOutput: false, + outputFilename: '3rdpartylicenses.txt', + skipChildCompilers: true, + })); + } + if (scriptsSourceMap || stylesSourceMap) { + const include = []; + if (scriptsSourceMap) { + include.push(/js$/); + } + if (stylesSourceMap) { + include.push(/css$/); + } + extraPlugins.push(new devtools_ignore_plugin_1.DevToolsIgnorePlugin()); + extraPlugins.push(new webpack_2.SourceMapDevToolPlugin({ + filename: '[file].map', + include, + // We want to set sourceRoot to `webpack:///` for non + // inline sourcemaps as otherwise paths to sourcemaps will be broken in browser + // `webpack:///` is needed for Visual Studio breakpoints to work properly as currently + // there is no way to set the 'webRoot' + sourceRoot: 'webpack:///', + moduleFilenameTemplate: '[resource-path]', + append: hiddenSourceMap ? false : undefined, + })); + } + if (verbose) { + extraPlugins.push(new watch_files_logs_plugin_1.WatchFilesLogsPlugin()); + } + if (buildOptions.statsJson) { + extraPlugins.push(new plugins_1.JsonStatsPlugin(path.resolve(root, buildOptions.outputPath, 'stats.json'))); + } + if (subresourceIntegrity) { + extraPlugins.push(new webpack_subresource_integrity_1.SubresourceIntegrityPlugin({ + hashFuncNames: ['sha384'], + })); + } + if (scriptsSourceMap || stylesSourceMap) { + extraRules.push({ + test: /\.[cm]?jsx?$/, + enforce: 'pre', + loader: require.resolve('source-map-loader'), + options: { + filterSourceMappingUrl: (_mapUri, resourcePath) => { + if (vendorSourceMap) { + // Consume all sourcemaps when vendor option is enabled. + return true; + } + // Don't consume sourcemaps in node_modules when vendor is disabled. + // But, do consume local libraries sourcemaps. + return !resourcePath.includes('node_modules'); + }, + }, + }); + } + if (main || polyfills) { + extraRules.push({ + test: tsConfig.options.allowJs ? /\.[cm]?[tj]sx?$/ : /\.[cm]?tsx?$/, + loader: webpack_1.AngularWebpackLoaderPath, + // The below are known paths that are not part of the TypeScript compilation even when allowJs is enabled. + exclude: [ + /[\\/]node_modules[/\\](?:css-loader|mini-css-extract-plugin|webpack-dev-server|webpack)[/\\]/, + ], + }); + extraPlugins.push((0, typescript_1.createIvyPlugin)(wco, aot, tsConfigPath)); + } + if (webWorkerTsConfig) { + extraPlugins.push((0, typescript_1.createIvyPlugin)(wco, false, path.resolve(wco.root, webWorkerTsConfig))); + } + const extraMinimizers = []; + if (scriptsOptimization) { + extraMinimizers.push(new plugins_1.JavaScriptOptimizerPlugin({ + define: { + ...(buildOptions.aot ? GLOBAL_DEFS_FOR_TERSER_WITH_AOT : GLOBAL_DEFS_FOR_TERSER), + 'ngServerMode': isPlatformServer, + }, + sourcemap: scriptsSourceMap, + supportedBrowsers: buildOptions.supportedBrowsers, + keepIdentifierNames: !environment_options_1.allowMangle || isPlatformServer, + removeLicenses: buildOptions.extractLicenses, + advanced: buildOptions.buildOptimizer, + })); + } + if (platform === 'browser' && (scriptsOptimization || stylesOptimization.minify)) { + extraMinimizers.push(new transfer_size_plugin_1.TransferSizePlugin()); + } + let crossOriginLoading = false; + if (subresourceIntegrity && crossOrigin === 'none') { + crossOriginLoading = 'anonymous'; + } + else if (crossOrigin !== 'none') { + crossOriginLoading = crossOrigin; + } + return { + mode: scriptsOptimization || stylesOptimization.minify ? 'production' : 'development', + devtool: false, + target: [isPlatformServer ? 'node' : 'web', 'es2015'], + profile: buildOptions.statsJson, + resolve: { + roots: [projectRoot], + extensions: ['.ts', '.tsx', '.mjs', '.js'], + symlinks: !buildOptions.preserveSymlinks, + modules: [tsConfig.options.baseUrl || projectRoot, 'node_modules'], + mainFields: isPlatformServer + ? ['es2020', 'es2015', 'module', 'main'] + : ['es2020', 'es2015', 'browser', 'module', 'main'], + conditionNames: ['es2020', 'es2015', '...'], + }, + resolveLoader: { + symlinks: !buildOptions.preserveSymlinks, + }, + context: root, + entry: entryPoints, + externals: externalDependencies, + output: { + uniqueName: projectName, + hashFunction: 'xxhash64', // todo: remove in webpack 6. This is part of `futureDefaults`. + clean: buildOptions.deleteOutputPath ?? true, + path: path.resolve(root, buildOptions.outputPath), + publicPath: buildOptions.deployUrl ?? '', + filename: `[name]${hashFormat.chunk}.js`, + chunkFilename: `[name]${hashFormat.chunk}.js`, + libraryTarget: isPlatformServer ? 'commonjs' : undefined, + crossOriginLoading, + trustedTypes: 'angular#bundler', + scriptType: 'module', + }, + watch: buildOptions.watch, + watchOptions: { + poll, + // The below is needed as when preserveSymlinks is enabled we disable `resolve.symlinks`. + followSymlinks: buildOptions.preserveSymlinks, + ignored: poll === undefined ? undefined : '**/node_modules/**', + }, + snapshot: { + module: { + // Use hash of content instead of timestamp because the timestamp of the symlink will be used + // instead of the referenced files which causes changes in symlinks not to be picked up. + hash: buildOptions.preserveSymlinks, + }, + }, + performance: { + hints: false, + }, + ignoreWarnings: [ + // https://github.com/webpack-contrib/source-map-loader/blob/b2de4249c7431dd8432da607e08f0f65e9d64219/src/index.js#L83 + /Failed to parse source map from/, + // https://github.com/webpack-contrib/postcss-loader/blob/bd261875fdf9c596af4ffb3a1a73fe3c549befda/src/index.js#L153-L158 + /Add postcss as project dependency/, + // esbuild will issue a warning, while still hoists the @charset at the very top. + // This is caused by a bug in css-loader https://github.com/webpack-contrib/css-loader/issues/1212 + /"@charset" must be the first rule in the file/, + ], + module: { + // Show an error for missing exports instead of a warning. + strictExportPresence: true, + parser: { + javascript: { + requireContext: false, + // Disable auto URL asset module creation. This doesn't effect `new Worker(new URL(...))` + // https://webpack.js.org/guides/asset-modules/#url-assets + url: false, + worker: !!webWorkerTsConfig, + }, + }, + rules: [ + { + test: /\.?(svg|html)$/, + // Only process HTML and SVG which are known Angular component resources. + resourceQuery: /\?ngResource/, + type: 'asset/source', + }, + { + // Mark files inside `rxjs/add` as containing side effects. + // If this is fixed upstream and the fixed version becomes the minimum + // supported version, this can be removed. + test: /[/\\]rxjs[/\\]add[/\\].+\.js$/, + sideEffects: true, + }, + { + test: /\.[cm]?[tj]sx?$/, + // The below is needed due to a bug in `@babel/runtime`. See: https://github.com/babel/babel/issues/12824 + resolve: { fullySpecified: false }, + exclude: [ + /[\\/]node_modules[/\\](?:core-js|@babel|tslib|web-animations-js|web-streams-polyfill|whatwg-url)[/\\]/, + ], + use: [ + { + loader: require.resolve('../../babel/webpack-loader'), + options: { + cacheDirectory: (cache.enabled && path.join(cache.path, 'babel-webpack')) || false, + aot: buildOptions.aot, + optimize: buildOptions.buildOptimizer, + supportedBrowsers: buildOptions.supportedBrowsers, + instrumentCode: codeCoverage + ? { + includedBasePath: sourceRoot ?? projectRoot, + excludedPaths: (0, helpers_1.getInstrumentationExcludedPaths)(root, codeCoverageExclude), + } + : undefined, + }, + }, + ], + }, + ...extraRules, + ], + }, + experiments: { + backCompat: false, + syncWebAssembly: true, + asyncWebAssembly: true, + topLevelAwait: false, + }, + infrastructureLogging: { + debug: verbose, + level: verbose ? 'verbose' : 'error', + }, + stats: (0, helpers_1.getStatsOptions)(verbose), + cache: (0, helpers_1.getCacheSettings)(wco, NG_VERSION.full), + optimization: { + minimizer: extraMinimizers, + moduleIds: 'deterministic', + chunkIds: buildOptions.namedChunks ? 'named' : 'deterministic', + emitOnErrors: false, + runtimeChunk: isPlatformServer ? false : 'single', + splitChunks: { + maxAsyncRequests: Infinity, + cacheGroups: { + default: !!commonChunk && { + chunks: 'async', + minChunks: 2, + priority: 10, + }, + common: !!commonChunk && { + name: 'common', + chunks: 'async', + minChunks: 2, + enforce: true, + priority: 5, + }, + vendors: false, + defaultVendors: !!vendorChunk && { + name: 'vendor', + chunks: (chunk) => chunk.name === 'main', + enforce: true, + test: VENDORS_TEST, + }, + }, + }, + }, + plugins: [ + new named_chunks_plugin_1.NamedChunksPlugin(), + new occurrences_plugin_1.OccurrencesPlugin({ + aot, + scriptsOptimization, + }), + new plugins_1.DedupeModuleResolvePlugin({ verbose }), + ...extraPlugins, + ], + node: false, + }; +} diff --git a/src/tools/webpack/configs/dev-server.d.ts b/src/tools/webpack/configs/dev-server.d.ts new file mode 100644 index 000000000..90b90ee12 --- /dev/null +++ b/src/tools/webpack/configs/dev-server.d.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { logging } from '@angular-devkit/core'; +import { Configuration } from 'webpack'; +import { WebpackConfigOptions, WebpackDevServerOptions } from '../../../utils/build-options'; +export declare function getDevServerConfig(wco: WebpackConfigOptions): Promise; +/** + * Resolve and build a URL _path_ that will be the root of the server. This resolved base href and + * deploy URL from the browser options and returns a path from the root. + */ +export declare function buildServePath(options: WebpackDevServerOptions, logger: logging.LoggerApi): string; diff --git a/src/tools/webpack/configs/dev-server.js b/src/tools/webpack/configs/dev-server.js new file mode 100644 index 000000000..1bdd1df3e --- /dev/null +++ b/src/tools/webpack/configs/dev-server.js @@ -0,0 +1,324 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getDevServerConfig = getDevServerConfig; +exports.buildServePath = buildServePath; +const core_1 = require("@angular-devkit/core"); +const node_fs_1 = require("node:fs"); +const node_path_1 = require("node:path"); +const node_url_1 = require("node:url"); +const error_1 = require("../../../utils/error"); +const load_esm_1 = require("../../../utils/load-esm"); +const webpack_browser_config_1 = require("../../../utils/webpack-browser-config"); +const hmr_loader_1 = require("../plugins/hmr/hmr-loader"); +async function getDevServerConfig(wco) { + const { buildOptions: { host, port, index, headers, watch, hmr, main, liveReload, proxyConfig }, logger, root, } = wco; + const servePath = buildServePath(wco.buildOptions, logger); + const extraRules = []; + if (hmr) { + extraRules.push({ + loader: hmr_loader_1.HmrLoader, + include: [(0, node_path_1.resolve)(wco.root, main)], + }); + } + const extraPlugins = []; + if (!watch) { + // There's no option to turn off file watching in webpack-dev-server, but + // we can override the file watcher instead. + extraPlugins.push({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + apply: (compiler) => { + compiler.hooks.afterEnvironment.tap('angular-cli', () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + compiler.watchFileSystem = { watch: () => { } }; + }); + }, + }); + } + return { + plugins: extraPlugins, + module: { + rules: extraRules, + }, + devServer: { + host, + port, + headers: { + 'Access-Control-Allow-Origin': '*', + ...headers, + }, + historyApiFallback: !!index && { + index: node_path_1.posix.join(servePath, (0, webpack_browser_config_1.getIndexOutputFile)(index)), + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + rewrites: [ + { + from: new RegExp(`^(?!${servePath})/.*`), + to: (context) => context.parsedUrl.href, + }, + ], + }, + // When setupExitSignals is enabled webpack-dev-server will shutdown gracefully which would + // require CTRL+C to be pressed multiple times to exit. + // See: https://github.com/webpack/webpack-dev-server/blob/c76b6d11a3821436c5e20207c8a38deb6ab7e33c/lib/Server.js#L1801-L1827 + setupExitSignals: false, + compress: false, + static: false, + server: getServerConfig(root, wco.buildOptions), + allowedHosts: getAllowedHostsConfig(wco.buildOptions), + devMiddleware: { + publicPath: servePath, + stats: false, + }, + liveReload, + hot: hmr && !liveReload ? 'only' : hmr, + proxy: await addProxyConfig(root, proxyConfig), + ...getWebSocketSettings(wco.buildOptions, servePath), + }, + }; +} +/** + * Resolve and build a URL _path_ that will be the root of the server. This resolved base href and + * deploy URL from the browser options and returns a path from the root. + */ +function buildServePath(options, logger) { + let servePath = options.servePath; + if (servePath === undefined) { + const defaultPath = findDefaultServePath(options.baseHref, options.deployUrl); + if (defaultPath == null) { + logger.warn(core_1.tags.oneLine ` + Warning: --deploy-url and/or --base-href contain unsupported values for ng serve. Default + serve path of '/' used. Use --serve-path to override. + `); + } + servePath = defaultPath || ''; + } + if (servePath.endsWith('/')) { + servePath = servePath.slice(0, -1); + } + if (!servePath.startsWith('/')) { + servePath = `/${servePath}`; + } + return servePath; +} +/** + * Private method to enhance a webpack config with SSL configuration. + * @private + */ +function getServerConfig(root, options) { + const { ssl, sslCert, sslKey } = options; + if (!ssl) { + return 'http'; + } + return { + type: 'https', + options: sslCert && sslKey + ? { + key: (0, node_path_1.resolve)(root, sslKey), + cert: (0, node_path_1.resolve)(root, sslCert), + } + : undefined, + }; +} +/** + * Private method to enhance a webpack config with Proxy configuration. + * @private + */ +async function addProxyConfig(root, proxyConfig) { + if (!proxyConfig) { + return undefined; + } + const proxyPath = (0, node_path_1.resolve)(root, proxyConfig); + if (!(0, node_fs_1.existsSync)(proxyPath)) { + throw new Error(`Proxy configuration file ${proxyPath} does not exist.`); + } + let proxyConfiguration; + switch ((0, node_path_1.extname)(proxyPath)) { + case '.json': { + const content = await node_fs_1.promises.readFile(proxyPath, 'utf-8'); + const { parse, printParseErrorCode } = await Promise.resolve().then(() => __importStar(require('jsonc-parser'))); + const parseErrors = []; + proxyConfiguration = parse(content, parseErrors, { allowTrailingComma: true }); + if (parseErrors.length > 0) { + let errorMessage = `Proxy configuration file ${proxyPath} contains parse errors:`; + for (const parseError of parseErrors) { + const { line, column } = getJsonErrorLineColumn(parseError.offset, content); + errorMessage += `\n[${line}, ${column}] ${printParseErrorCode(parseError.error)}`; + } + throw new Error(errorMessage); + } + break; + } + case '.mjs': + // Load the ESM configuration file using the TypeScript dynamic import workaround. + // Once TypeScript provides support for keeping the dynamic import this workaround can be + // changed to a direct dynamic import. + proxyConfiguration = await (0, load_esm_1.loadEsmModule)((0, node_url_1.pathToFileURL)(proxyPath)); + break; + case '.cjs': + proxyConfiguration = require(proxyPath); + break; + default: + // The file could be either CommonJS or ESM. + // CommonJS is tried first then ESM if loading fails. + try { + proxyConfiguration = require(proxyPath); + } + catch (e) { + (0, error_1.assertIsError)(e); + if (e.code !== 'ERR_REQUIRE_ESM' && e.code !== 'ERR_REQUIRE_ASYNC_MODULE') { + throw e; + } + // Load the ESM configuration file using the TypeScript dynamic import workaround. + // Once TypeScript provides support for keeping the dynamic import this workaround can be + // changed to a direct dynamic import. + proxyConfiguration = await (0, load_esm_1.loadEsmModule)((0, node_url_1.pathToFileURL)(proxyPath)); + } + } + if ('default' in proxyConfiguration) { + proxyConfiguration = proxyConfiguration.default; + } + return normalizeProxyConfiguration(proxyConfiguration); +} +/** + * Calculates the line and column for an error offset in the content of a JSON file. + * @param location The offset error location from the beginning of the content. + * @param content The full content of the file containing the error. + * @returns An object containing the line and column + */ +function getJsonErrorLineColumn(offset, content) { + if (offset === 0) { + return { line: 1, column: 1 }; + } + let line = 0; + let position = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + ++line; + const nextNewline = content.indexOf('\n', position); + if (nextNewline === -1 || nextNewline > offset) { + break; + } + position = nextNewline + 1; + } + return { line, column: offset - position + 1 }; +} +/** + * Find the default server path. We don't want to expose baseHref and deployUrl as arguments, only + * the browser options where needed. This method should stay private (people who want to resolve + * baseHref and deployUrl should use the buildServePath exported function. + * @private + */ +function findDefaultServePath(baseHref, deployUrl) { + if (!baseHref && !deployUrl) { + return ''; + } + if (/^(\w+:)?\/\//.test(baseHref || '') || /^(\w+:)?\/\//.test(deployUrl || '')) { + // If baseHref or deployUrl is absolute, unsupported by ng serve + return null; + } + // normalize baseHref + // for ng serve the starting base is always `/` so a relative + // and root relative value are identical + const baseHrefParts = (baseHref || '').split('/').filter((part) => part !== ''); + if (baseHref && !baseHref.endsWith('/')) { + baseHrefParts.pop(); + } + const normalizedBaseHref = baseHrefParts.length === 0 ? '/' : `/${baseHrefParts.join('/')}/`; + if (deployUrl && deployUrl[0] === '/') { + if (baseHref && baseHref[0] === '/' && normalizedBaseHref !== deployUrl) { + // If baseHref and deployUrl are root relative and not equivalent, unsupported by ng serve + return null; + } + return deployUrl; + } + // Join together baseHref and deployUrl + return `${normalizedBaseHref}${deployUrl || ''}`; +} +function getAllowedHostsConfig(options) { + if (options.disableHostCheck) { + return 'all'; + } + else if (options.allowedHosts?.length) { + return options.allowedHosts; + } + return undefined; +} +function getWebSocketSettings(options, servePath) { + const { hmr, liveReload } = options; + if (!hmr && !liveReload) { + return { + webSocketServer: false, + client: undefined, + }; + } + const webSocketPath = node_path_1.posix.join(servePath, 'ng-cli-ws'); + return { + webSocketServer: { + options: { + path: webSocketPath, + }, + }, + client: { + logging: 'info', + webSocketURL: getPublicHostOptions(options, webSocketPath), + overlay: { + errors: true, + warnings: false, + runtimeErrors: false, + }, + }, + }; +} +function getPublicHostOptions(options, webSocketPath) { + let publicHost = options.publicHost; + if (publicHost) { + const hostWithProtocol = !/^\w+:\/\//.test(publicHost) ? `https://${publicHost}` : publicHost; + publicHost = new node_url_1.URL(hostWithProtocol).host; + } + return `auto://${publicHost || '0.0.0.0:0'}${webSocketPath}`; +} +function normalizeProxyConfiguration(proxy) { + return Array.isArray(proxy) + ? proxy + : Object.entries(proxy).map(([context, value]) => ({ context: [context], ...value })); +} diff --git a/src/tools/webpack/configs/index.d.ts b/src/tools/webpack/configs/index.d.ts new file mode 100644 index 000000000..bd469b5b2 --- /dev/null +++ b/src/tools/webpack/configs/index.d.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export * from './common'; +export * from './dev-server'; +export * from './styles'; diff --git a/src/tools/webpack/configs/index.js b/src/tools/webpack/configs/index.js new file mode 100644 index 000000000..398196147 --- /dev/null +++ b/src/tools/webpack/configs/index.js @@ -0,0 +1,26 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./common"), exports); +__exportStar(require("./dev-server"), exports); +__exportStar(require("./styles"), exports); diff --git a/src/tools/webpack/configs/styles.d.ts b/src/tools/webpack/configs/styles.d.ts new file mode 100644 index 000000000..d212138c1 --- /dev/null +++ b/src/tools/webpack/configs/styles.d.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { Configuration } from 'webpack'; +import { WebpackConfigOptions } from '../../../utils/build-options'; +export declare function getStylesConfig(wco: WebpackConfigOptions): Promise; diff --git a/src/tools/webpack/configs/styles.js b/src/tools/webpack/configs/styles.js new file mode 100644 index 000000000..d14c37e22 --- /dev/null +++ b/src/tools/webpack/configs/styles.js @@ -0,0 +1,383 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getStylesConfig = getStylesConfig; +const private_1 = require("@angular/build/private"); +const mini_css_extract_plugin_1 = __importDefault(require("mini-css-extract-plugin")); +const node_module_1 = require("node:module"); +const path = __importStar(require("node:path")); +const node_url_1 = require("node:url"); +const plugins_1 = require("../plugins"); +const css_optimizer_plugin_1 = require("../plugins/css-optimizer-plugin"); +const styles_webpack_plugin_1 = require("../plugins/styles-webpack-plugin"); +const helpers_1 = require("../utils/helpers"); +// eslint-disable-next-line max-lines-per-function +async function getStylesConfig(wco) { + const { root, buildOptions, logger, projectRoot } = wco; + const extraPlugins = []; + extraPlugins.push(new plugins_1.AnyComponentStyleBudgetChecker(buildOptions.budgets)); + const cssSourceMap = buildOptions.sourceMap.styles; + // Determine hashing format. + const hashFormat = (0, helpers_1.getOutputHashFormat)(buildOptions.outputHashing); + // use includePaths from appConfig + const includePaths = buildOptions.stylePreprocessorOptions?.includePaths?.map((p) => path.resolve(root, p)) ?? []; + // Process global styles. + if (buildOptions.styles.length > 0) { + const { entryPoints, noInjectNames } = (0, helpers_1.normalizeGlobalStyles)(buildOptions.styles); + extraPlugins.push(new styles_webpack_plugin_1.StylesWebpackPlugin({ + root, + entryPoints, + preserveSymlinks: buildOptions.preserveSymlinks, + })); + if (noInjectNames.length > 0) { + // Add plugin to remove hashes from lazy styles. + extraPlugins.push(new plugins_1.RemoveHashPlugin({ chunkNames: noInjectNames, hashFormat })); + } + } + const sassImplementation = new private_1.SassWorkerImplementation(); + extraPlugins.push({ + apply(compiler) { + compiler.hooks.shutdown.tap('sass-worker', () => { + void sassImplementation.close(); + }); + }, + }); + const assetNameTemplate = (0, helpers_1.assetNameTemplateFactory)(hashFormat); + const extraPostcssPlugins = []; + const searchDirectories = await (0, private_1.generateSearchDirectories)([projectRoot, root]); + const postcssConfig = await (0, private_1.loadPostcssConfiguration)(searchDirectories); + if (postcssConfig) { + const postCssPluginRequire = (0, node_module_1.createRequire)(path.dirname(postcssConfig.configPath) + '/'); + for (const [pluginName, pluginOptions] of postcssConfig.config.plugins) { + const pluginMod = postCssPluginRequire(pluginName); + const plugin = pluginMod.__esModule ? pluginMod['default'] : pluginMod; + if (typeof plugin !== 'function' || plugin.postcss !== true) { + throw new Error(`Attempted to load invalid Postcss plugin: "${pluginName}"`); + } + extraPostcssPlugins.push(plugin(pluginOptions)); + } + } + else { + // Attempt to setup Tailwind CSS + // Only load Tailwind CSS plugin if configuration file was found. + // This acts as a guard to ensure the project actually wants to use Tailwind CSS. + // The package may be unknowningly present due to a third-party transitive package dependency. + const tailwindConfigPath = (0, private_1.findTailwindConfiguration)(searchDirectories); + if (tailwindConfigPath) { + let tailwindPackagePath; + try { + tailwindPackagePath = require.resolve('tailwindcss', { paths: [root] }); + } + catch { + const relativeTailwindConfigPath = path.relative(root, tailwindConfigPath); + logger.warn(`Tailwind CSS configuration file found (${relativeTailwindConfigPath})` + + ` but the 'tailwindcss' package is not installed.` + + ` To enable Tailwind CSS, please install the 'tailwindcss' package.`); + } + if (tailwindPackagePath) { + extraPostcssPlugins.push(require(tailwindPackagePath)({ config: tailwindConfigPath })); + } + } + } + const autoprefixer = require('autoprefixer'); + const postcssOptionsCreator = (inlineSourcemaps, extracted) => { + const optionGenerator = (loader) => ({ + map: inlineSourcemaps + ? { + inline: true, + annotation: false, + } + : undefined, + plugins: [ + (0, plugins_1.PostcssCliResources)({ + baseHref: buildOptions.baseHref, + deployUrl: buildOptions.deployUrl, + resourcesOutputPath: buildOptions.resourcesOutputPath, + loader, + filename: assetNameTemplate, + emitFile: buildOptions.platform !== 'server', + extracted, + }), + ...extraPostcssPlugins, + autoprefixer({ + ignoreUnknownVersions: true, + overrideBrowserslist: buildOptions.supportedBrowsers, + }), + ], + }); + // postcss-loader fails when trying to determine configuration files for data URIs + optionGenerator.config = false; + return optionGenerator; + }; + let componentsSourceMap = !!cssSourceMap; + if (cssSourceMap) { + if (buildOptions.optimization.styles.minify) { + // Never use component css sourcemap when style optimizations are on. + // It will just increase bundle size without offering good debug experience. + logger.warn('Components styles sourcemaps are not generated when styles optimization is enabled.'); + componentsSourceMap = false; + } + else if (buildOptions.sourceMap.hidden) { + // Inline all sourcemap types except hidden ones, which are the same as no sourcemaps + // for component css. + logger.warn('Components styles sourcemaps are not generated when sourcemaps are hidden.'); + componentsSourceMap = false; + } + } + // extract global css from js files into own css file. + extraPlugins.push(new mini_css_extract_plugin_1.default({ filename: `[name]${hashFormat.extract}.css` })); + if (!buildOptions.hmr) { + // don't remove `.js` files for `.css` when we are using HMR these contain HMR accept codes. + // suppress empty .js files in css only entry points. + extraPlugins.push(new plugins_1.SuppressExtractedTextChunksWebpackPlugin()); + } + const postCss = require('postcss'); + const postCssLoaderPath = require.resolve('postcss-loader'); + const componentStyleLoaders = [ + { + loader: require.resolve('css-loader'), + options: { + url: false, + sourceMap: componentsSourceMap, + importLoaders: 1, + exportType: 'string', + esModule: false, + }, + }, + { + loader: postCssLoaderPath, + options: { + implementation: postCss, + postcssOptions: postcssOptionsCreator(componentsSourceMap, false), + }, + }, + ]; + const globalStyleLoaders = [ + { + loader: mini_css_extract_plugin_1.default.loader, + }, + { + loader: require.resolve('css-loader'), + options: { + url: false, + sourceMap: !!cssSourceMap, + importLoaders: 1, + }, + }, + { + loader: postCssLoaderPath, + options: { + implementation: postCss, + postcssOptions: postcssOptionsCreator(false, true), + sourceMap: !!cssSourceMap, + }, + }, + ]; + const styleLanguages = [ + { + extensions: ['css'], + use: [], + }, + { + extensions: ['scss'], + use: [ + { + loader: require.resolve('resolve-url-loader'), + options: { + sourceMap: cssSourceMap, + }, + }, + { + loader: require.resolve('sass-loader'), + options: getSassLoaderOptions(root, sassImplementation, includePaths, false, !!buildOptions.verbose, !!buildOptions.preserveSymlinks), + }, + ], + }, + { + extensions: ['sass'], + use: [ + { + loader: require.resolve('resolve-url-loader'), + options: { + sourceMap: cssSourceMap, + }, + }, + { + loader: require.resolve('sass-loader'), + options: getSassLoaderOptions(root, sassImplementation, includePaths, true, !!buildOptions.verbose, !!buildOptions.preserveSymlinks), + }, + ], + }, + { + extensions: ['less'], + use: [ + { + loader: require.resolve('less-loader'), + options: { + implementation: require('less'), + sourceMap: cssSourceMap, + lessOptions: { + javascriptEnabled: true, + paths: includePaths, + }, + }, + }, + ], + }, + ]; + return { + module: { + rules: styleLanguages.map(({ extensions, use }) => ({ + test: new RegExp(`\\.(?:${extensions.join('|')})$`, 'i'), + rules: [ + // Setup processing rules for global and component styles + { + oneOf: [ + // Global styles are only defined global styles + { + use: globalStyleLoaders, + resourceQuery: /\?ngGlobalStyle/, + }, + // Component styles are all styles except defined global styles + { + use: componentStyleLoaders, + resourceQuery: /\?ngResource/, + }, + ], + }, + { use }, + ], + })), + }, + optimization: { + minimizer: buildOptions.optimization.styles.minify + ? [ + new css_optimizer_plugin_1.CssOptimizerPlugin({ + supportedBrowsers: buildOptions.supportedBrowsers, + }), + ] + : undefined, + }, + plugins: extraPlugins, + }; +} +function getSassLoaderOptions(root, implementation, includePaths, indentedSyntax, verbose, preserveSymlinks) { + return { + sourceMap: true, + api: 'modern', + implementation, + // Webpack importer is only implemented in the legacy API and we have our own custom Webpack importer. + // See: https://github.com/webpack-contrib/sass-loader/blob/997f3eb41d86dd00d5fa49c395a1aeb41573108c/src/utils.js#L642-L651 + webpackImporter: false, + sassOptions: (loaderContext) => ({ + importers: [getSassResolutionImporter(loaderContext, root, preserveSymlinks)], + loadPaths: includePaths, + // Use expanded as otherwise sass will remove comments that are needed for autoprefixer + // Ex: /* autoprefixer grid: autoplace */ + // See: https://github.com/webpack-contrib/sass-loader/blob/45ad0be17264ceada5f0b4fb87e9357abe85c4ff/src/getSassOptions.js#L68-L70 + style: 'expanded', + // Silences compiler warnings from 3rd party stylesheets + quietDeps: !verbose, + verbose, + syntax: indentedSyntax ? 'indented' : 'scss', + sourceMapIncludeSources: true, + }), + }; +} +function getSassResolutionImporter(loaderContext, root, preserveSymlinks) { + const commonResolverOptions = { + conditionNames: ['sass', 'style'], + mainFields: ['sass', 'style', 'main', '...'], + extensions: ['.scss', '.sass', '.css'], + restrictions: [/\.((sa|sc|c)ss)$/i], + preferRelative: true, + symlinks: !preserveSymlinks, + }; + // Sass also supports import-only files. If you name a file .import.scss, it will only be loaded for imports, not for @uses. + // See: https://sass-lang.com/documentation/at-rules/import#import-only-files + const resolveImport = loaderContext.getResolve({ + ...commonResolverOptions, + dependencyType: 'sass-import', + mainFiles: ['_index.import', '_index', 'index.import', 'index', '...'], + }); + const resolveModule = loaderContext.getResolve({ + ...commonResolverOptions, + dependencyType: 'sass-module', + mainFiles: ['_index', 'index', '...'], + }); + return { + findFileUrl: async (url, { fromImport, containingUrl }) => { + if (url.charAt(0) === '.') { + // Let Sass handle relative imports. + return null; + } + let resolveDir = root; + if (containingUrl) { + resolveDir = path.dirname((0, node_url_1.fileURLToPath)(containingUrl)); + } + const resolve = fromImport ? resolveImport : resolveModule; + // Try to resolve from root of workspace + const result = await tryResolve(resolve, resolveDir, url); + return result ? (0, node_url_1.pathToFileURL)(result) : null; + }, + }; +} +async function tryResolve(resolve, root, url) { + try { + return await resolve(root, url); + } + catch { + // Try to resolve a partial file + // @use '@material/button/button' as mdc-button; + // `@material/button/button` -> `@material/button/_button` + const lastSlashIndex = url.lastIndexOf('/'); + const underscoreIndex = lastSlashIndex + 1; + if (underscoreIndex > 0 && url.charAt(underscoreIndex) !== '_') { + const partialFileUrl = `${url.slice(0, underscoreIndex)}_${url.slice(underscoreIndex)}`; + return resolve(root, partialFileUrl).catch(() => undefined); + } + } + return undefined; +} diff --git a/src/angular-cli-files/plugins/any-component-style-budget-checker.d.ts b/src/tools/webpack/plugins/any-component-style-budget-checker.d.ts similarity index 50% rename from src/angular-cli-files/plugins/any-component-style-budget-checker.d.ts rename to src/tools/webpack/plugins/any-component-style-budget-checker.d.ts index f866ccec5..080672f70 100644 --- a/src/angular-cli-files/plugins/any-component-style-budget-checker.d.ts +++ b/src/tools/webpack/plugins/any-component-style-budget-checker.d.ts @@ -1,18 +1,18 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { Compiler, Plugin } from 'webpack'; -import { Budget } from '../../../src/browser/schema'; +import { BudgetEntry } from '@angular/build/private'; +import { Compiler } from 'webpack'; /** * Check budget sizes for component styles by emitting a warning or error if a * budget is exceeded by a particular component's styles. */ -export declare class AnyComponentStyleBudgetChecker implements Plugin { +export declare class AnyComponentStyleBudgetChecker { private readonly budgets; - constructor(budgets: Budget[]); + constructor(budgets: BudgetEntry[]); apply(compiler: Compiler): void; } diff --git a/src/tools/webpack/plugins/any-component-style-budget-checker.js b/src/tools/webpack/plugins/any-component-style-budget-checker.js new file mode 100644 index 000000000..b856020b3 --- /dev/null +++ b/src/tools/webpack/plugins/any-component-style-budget-checker.js @@ -0,0 +1,99 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AnyComponentStyleBudgetChecker = void 0; +const private_1 = require("@angular/build/private"); +const path = __importStar(require("node:path")); +const webpack_1 = require("webpack"); +const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); +const PLUGIN_NAME = 'AnyComponentStyleBudgetChecker'; +/** + * Check budget sizes for component styles by emitting a warning or error if a + * budget is exceeded by a particular component's styles. + */ +class AnyComponentStyleBudgetChecker { + budgets; + constructor(budgets) { + this.budgets = budgets.filter((budget) => budget.type === private_1.BudgetType.AnyComponentStyle); + } + apply(compiler) { + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.processAssets.tap({ + name: PLUGIN_NAME, + stage: webpack_1.Compilation.PROCESS_ASSETS_STAGE_ANALYSE, + }, () => { + // No budgets. + if (this.budgets.length === 0) { + return; + } + // In AOT compilations component styles get processed in child compilations. + if (!compilation.compiler.parentCompilation) { + return; + } + const cssExtensions = ['.css', '.scss', '.less', '.sass']; + const componentStyles = Object.keys(compilation.assets) + .filter((name) => cssExtensions.includes(path.extname(name))) + .map((name) => ({ + name, + size: compilation.assets[name].size(), + componentStyle: true, + })); + for (const { severity, message } of (0, private_1.checkBudgets)(this.budgets, { chunks: [], assets: componentStyles }, true)) { + switch (severity) { + case private_1.ThresholdSeverity.Warning: + (0, webpack_diagnostics_1.addWarning)(compilation, message); + break; + case private_1.ThresholdSeverity.Error: + (0, webpack_diagnostics_1.addError)(compilation, message); + break; + default: + assertNever(severity); + } + } + }); + }); + } +} +exports.AnyComponentStyleBudgetChecker = AnyComponentStyleBudgetChecker; +function assertNever(input) { + throw new Error(`Unexpected call to assertNever() with input: ${JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`); +} diff --git a/src/tools/webpack/plugins/builder-watch-plugin.d.ts b/src/tools/webpack/plugins/builder-watch-plugin.d.ts new file mode 100644 index 000000000..0005ad0ac --- /dev/null +++ b/src/tools/webpack/plugins/builder-watch-plugin.d.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Compiler } from 'webpack'; +export type BuilderWatcherCallback = (events: Array<{ + path: string; + type: 'created' | 'modified' | 'deleted'; + time?: number; +}>) => void; +export interface BuilderWatcherFactory { + watch(files: Iterable, directories: Iterable, callback: BuilderWatcherCallback): { + close(): void; + }; +} +export declare class BuilderWatchPlugin { + private readonly watcherFactory; + constructor(watcherFactory: BuilderWatcherFactory); + apply(compiler: Compiler): void; +} diff --git a/src/tools/webpack/plugins/builder-watch-plugin.js b/src/tools/webpack/plugins/builder-watch-plugin.js new file mode 100644 index 000000000..4402b63a4 --- /dev/null +++ b/src/tools/webpack/plugins/builder-watch-plugin.js @@ -0,0 +1,101 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BuilderWatchPlugin = void 0; +class TimeInfoMap extends Map { + update(path, timestamp) { + this.set(path, Object.freeze({ safeTime: timestamp, timestamp })); + } + toTimestamps() { + const timestamps = new Map(); + for (const [file, entry] of this) { + timestamps.set(file, entry.timestamp); + } + return timestamps; + } +} +class BuilderWatchFileSystem { + watcherFactory; + inputFileSystem; + constructor(watcherFactory, inputFileSystem) { + this.watcherFactory = watcherFactory; + this.inputFileSystem = inputFileSystem; + } + watch(files, directories, missing, startTime, _options, callback, callbackUndelayed) { + const watchedFiles = new Set(files); + const watchedDirectories = new Set(directories); + const watchedMissing = new Set(missing); + const timeInfo = new TimeInfoMap(); + for (const file of files) { + timeInfo.update(file, startTime); + } + for (const directory of directories) { + timeInfo.update(directory, startTime); + } + const watcher = this.watcherFactory.watch(files, directories, (events) => { + if (events.length === 0) { + return; + } + if (callbackUndelayed) { + process.nextTick(() => callbackUndelayed(events[0].path, events[0].time ?? Date.now())); + } + process.nextTick(() => { + const removals = new Set(); + const fileChanges = new Set(); + const directoryChanges = new Set(); + const missingChanges = new Set(); + for (const event of events) { + this.inputFileSystem?.purge?.(event.path); + if (event.type === 'deleted') { + timeInfo.delete(event.path); + removals.add(event.path); + } + else { + timeInfo.update(event.path, event.time ?? Date.now()); + if (watchedFiles.has(event.path)) { + fileChanges.add(event.path); + } + else if (watchedDirectories.has(event.path)) { + directoryChanges.add(event.path); + } + else if (watchedMissing.has(event.path)) { + missingChanges.add(event.path); + } + } + } + const timeInfoMap = new Map(timeInfo); + callback(null, timeInfoMap, timeInfoMap, new Set([...fileChanges, ...directoryChanges, ...missingChanges]), removals); + }); + }); + return { + close() { + watcher.close(); + }, + pause() { }, + getFileTimeInfoEntries() { + return new Map(timeInfo); + }, + getContextTimeInfoEntries() { + return new Map(timeInfo); + }, + }; + } +} +class BuilderWatchPlugin { + watcherFactory; + constructor(watcherFactory) { + this.watcherFactory = watcherFactory; + } + apply(compiler) { + compiler.hooks.environment.tap('BuilderWatchPlugin', () => { + compiler.watchFileSystem = new BuilderWatchFileSystem(this.watcherFactory, compiler.inputFileSystem); + }); + } +} +exports.BuilderWatchPlugin = BuilderWatchPlugin; diff --git a/src/angular-cli-files/plugins/common-js-usage-warn-plugin.d.ts b/src/tools/webpack/plugins/common-js-usage-warn-plugin.d.ts similarity index 55% rename from src/angular-cli-files/plugins/common-js-usage-warn-plugin.d.ts rename to src/tools/webpack/plugins/common-js-usage-warn-plugin.d.ts index 7adc54d8d..44f8f8f8a 100644 --- a/src/angular-cli-files/plugins/common-js-usage-warn-plugin.d.ts +++ b/src/tools/webpack/plugins/common-js-usage-warn-plugin.d.ts @@ -1,20 +1,21 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Compiler } from 'webpack'; export interface CommonJsUsageWarnPluginOptions { - /** A list of CommonJS packages that are allowed to be used without a warning. */ - allowedDepedencies?: string[]; + /** A list of CommonJS or AMD packages that are allowed to be used without a warning. Use `'*'` to allow all. */ + allowedDependencies?: string[]; } export declare class CommonJsUsageWarnPlugin { private options; private shownWarnings; - private allowedDepedencies; + private allowedDependencies; constructor(options?: CommonJsUsageWarnPluginOptions); apply(compiler: Compiler): void; private hasCommonJsDependencies; + private rawRequestToPackageName; } diff --git a/src/tools/webpack/plugins/common-js-usage-warn-plugin.js b/src/tools/webpack/plugins/common-js-usage-warn-plugin.js new file mode 100644 index 000000000..acebffabd --- /dev/null +++ b/src/tools/webpack/plugins/common-js-usage-warn-plugin.js @@ -0,0 +1,125 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CommonJsUsageWarnPlugin = void 0; +const node_path_1 = require("node:path"); +const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); +// Webpack doesn't export these so the deep imports can potentially break. +const AMDDefineDependency = require('webpack/lib/dependencies/AMDDefineDependency'); +const CommonJsExportsDependency = require('webpack/lib/dependencies/CommonJsExportsDependency'); +const CommonJsRequireDependency = require('webpack/lib/dependencies/CommonJsRequireDependency'); +const CommonJsSelfReferenceDependency = require('webpack/lib/dependencies/CommonJsSelfReferenceDependency'); +class CommonJsUsageWarnPlugin { + options; + shownWarnings = new Set(); + allowedDependencies; + constructor(options = {}) { + this.options = options; + this.allowedDependencies = new Set(this.options.allowedDependencies); + } + apply(compiler) { + if (this.allowedDependencies.has('*')) { + return; + } + compiler.hooks.compilation.tap('CommonJsUsageWarnPlugin', (compilation) => { + compilation.hooks.finishModules.tap('CommonJsUsageWarnPlugin', (modules) => { + const mainEntry = compilation.entries.get('main'); + if (!mainEntry) { + return; + } + const mainModules = new Set(mainEntry.dependencies.map((dep) => compilation.moduleGraph.getModule(dep))); + for (const module of modules) { + const { dependencies, rawRequest } = module; + if (!rawRequest || + rawRequest.startsWith('.') || + (0, node_path_1.isAbsolute)(rawRequest) || + this.allowedDependencies.has(rawRequest) || + this.allowedDependencies.has(this.rawRequestToPackageName(rawRequest)) || + rawRequest.startsWith('@angular/common/locales/')) { + /** + * Skip when: + * - module is absolute or relative. + * - module is allowed even if it's a CommonJS. + * - module is a locale imported from '@angular/common'. + */ + continue; + } + if (this.hasCommonJsDependencies(compilation, dependencies)) { + // Dependency is CommonsJS or AMD. + const issuer = getIssuer(compilation, module); + // Check if it's parent issuer is also a CommonJS dependency. + // In case it is skip as an warning will be show for the parent CommonJS dependency. + const parentDependencies = getIssuer(compilation, issuer)?.dependencies; + if (parentDependencies && + this.hasCommonJsDependencies(compilation, parentDependencies, true)) { + continue; + } + // Find the main issuer (entry-point). + let mainIssuer = issuer; + let nextIssuer = getIssuer(compilation, mainIssuer); + while (nextIssuer) { + mainIssuer = nextIssuer; + nextIssuer = getIssuer(compilation, mainIssuer); + } + // Only show warnings for modules from main entrypoint. + // And if the issuer request is not from 'webpack-dev-server', as 'webpack-dev-server' + // will require CommonJS libraries for live reloading such as 'sockjs-node'. + if (mainIssuer && mainModules.has(mainIssuer)) { + const warning = `${issuer?.userRequest} depends on '${rawRequest}'. ` + + 'CommonJS or AMD dependencies can cause optimization bailouts.\n' + + 'For more info see: https://angular.dev/tools/cli/build#configuring-commonjs-dependencies'; + // Avoid showing the same warning multiple times when in 'watch' mode. + if (!this.shownWarnings.has(warning)) { + (0, webpack_diagnostics_1.addWarning)(compilation, warning); + this.shownWarnings.add(warning); + } + } + } + } + }); + }); + } + hasCommonJsDependencies(compilation, dependencies, checkParentModules = false) { + for (const dep of dependencies) { + if (dep instanceof CommonJsRequireDependency || + dep instanceof CommonJsExportsDependency || + dep instanceof CommonJsSelfReferenceDependency || + dep instanceof AMDDefineDependency) { + return true; + } + if (checkParentModules) { + const module = getWebpackModule(compilation, dep); + if (module && this.hasCommonJsDependencies(compilation, module.dependencies)) { + return true; + } + } + } + return false; + } + rawRequestToPackageName(rawRequest) { + return rawRequest.startsWith('@') + ? // Scoped request ex: @angular/common/locale/en -> @angular/common + rawRequest.split('/', 2).join('/') + : // Non-scoped request ex: lodash/isEmpty -> lodash + rawRequest.split('/', 1)[0]; + } +} +exports.CommonJsUsageWarnPlugin = CommonJsUsageWarnPlugin; +function getIssuer(compilation, module) { + if (!module) { + return null; + } + return compilation.moduleGraph.getIssuer(module); +} +function getWebpackModule(compilation, dependency) { + if (!dependency) { + return null; + } + return compilation.moduleGraph.getModule(dependency); +} diff --git a/src/tools/webpack/plugins/css-optimizer-plugin.d.ts b/src/tools/webpack/plugins/css-optimizer-plugin.d.ts new file mode 100644 index 000000000..fa8f6e253 --- /dev/null +++ b/src/tools/webpack/plugins/css-optimizer-plugin.d.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { Compiler } from 'webpack'; +export interface CssOptimizerPluginOptions { + supportedBrowsers?: string[]; +} +/** + * A Webpack plugin that provides CSS optimization capabilities. + * + * The plugin uses both `esbuild` to provide both fast and highly-optimized + * code output. + */ +export declare class CssOptimizerPlugin { + private targets; + private esbuild; + constructor(options?: CssOptimizerPluginOptions); + apply(compiler: Compiler): void; + /** + * Optimizes a CSS asset using esbuild. + * + * @param input The CSS asset source content to optimize. + * @param name The name of the CSS asset. Used to generate source maps. + * @param inputMap Optionally specifies the CSS asset's original source map that will + * be merged with the intermediate optimized source map. + * @param target Optionally specifies the target browsers for the output code. + * @returns A promise resolving to the optimized CSS, source map, and any warnings. + */ + private optimize; + private addWarnings; +} diff --git a/src/tools/webpack/plugins/css-optimizer-plugin.js b/src/tools/webpack/plugins/css-optimizer-plugin.js new file mode 100644 index 000000000..66c61d634 --- /dev/null +++ b/src/tools/webpack/plugins/css-optimizer-plugin.js @@ -0,0 +1,123 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CssOptimizerPlugin = void 0; +const private_1 = require("@angular/build/private"); +const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); +const esbuild_executor_1 = require("./esbuild-executor"); +/** + * The name of the plugin provided to Webpack when tapping Webpack compiler hooks. + */ +const PLUGIN_NAME = 'angular-css-optimizer'; +/** + * A Webpack plugin that provides CSS optimization capabilities. + * + * The plugin uses both `esbuild` to provide both fast and highly-optimized + * code output. + */ +class CssOptimizerPlugin { + targets; + esbuild = new esbuild_executor_1.EsbuildExecutor(); + constructor(options) { + if (options?.supportedBrowsers) { + this.targets = (0, private_1.transformSupportedBrowsersToTargets)(options.supportedBrowsers); + } + } + apply(compiler) { + const { OriginalSource, SourceMapSource } = compiler.webpack.sources; + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + const logger = compilation.getLogger('build-angular.CssOptimizerPlugin'); + compilation.hooks.processAssets.tapPromise({ + name: PLUGIN_NAME, + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, + }, async (compilationAssets) => { + const cache = compilation.options.cache && compilation.getCache(PLUGIN_NAME); + logger.time('optimize css assets'); + for (const assetName of Object.keys(compilationAssets)) { + if (!/\.(?:css|scss|sass|less)$/.test(assetName)) { + continue; + } + const asset = compilation.getAsset(assetName); + // Skip assets that have already been optimized or are verbatim copies (project assets) + if (!asset || asset.info.minimized || asset.info.copied) { + continue; + } + const { source: styleAssetSource, name } = asset; + let cacheItem; + if (cache) { + const eTag = cache.getLazyHashedEtag(styleAssetSource); + cacheItem = cache.getItemCache(name, eTag); + const cachedOutput = await cacheItem.getPromise(); + if (cachedOutput) { + logger.debug(`${name} restored from cache`); + await this.addWarnings(compilation, cachedOutput.warnings); + compilation.updateAsset(name, cachedOutput.source, (assetInfo) => ({ + ...assetInfo, + minimized: true, + })); + continue; + } + } + const { source, map: inputMap } = styleAssetSource.sourceAndMap(); + const input = typeof source === 'string' ? source : source.toString(); + const optimizeAssetLabel = `optimize asset: ${asset.name}`; + logger.time(optimizeAssetLabel); + const { code, warnings, map } = await this.optimize(input, asset.name, inputMap, this.targets); + logger.timeEnd(optimizeAssetLabel); + await this.addWarnings(compilation, warnings); + const optimizedAsset = map + ? new SourceMapSource(code, name, map) + : new OriginalSource(code, name); + compilation.updateAsset(name, optimizedAsset, (assetInfo) => ({ + ...assetInfo, + minimized: true, + })); + await cacheItem?.storePromise({ + source: optimizedAsset, + warnings, + }); + } + logger.timeEnd('optimize css assets'); + }); + }); + } + /** + * Optimizes a CSS asset using esbuild. + * + * @param input The CSS asset source content to optimize. + * @param name The name of the CSS asset. Used to generate source maps. + * @param inputMap Optionally specifies the CSS asset's original source map that will + * be merged with the intermediate optimized source map. + * @param target Optionally specifies the target browsers for the output code. + * @returns A promise resolving to the optimized CSS, source map, and any warnings. + */ + optimize(input, name, inputMap, target) { + let sourceMapLine; + if (inputMap) { + // esbuild will automatically remap the sourcemap if provided + sourceMapLine = `\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from(JSON.stringify(inputMap)).toString('base64')} */`; + } + return this.esbuild.transform(sourceMapLine ? input + sourceMapLine : input, { + loader: 'css', + legalComments: 'inline', + minify: true, + sourcemap: !!inputMap && 'external', + sourcefile: name, + target, + }); + } + async addWarnings(compilation, warnings) { + if (warnings.length > 0) { + for (const warning of await this.esbuild.formatMessages(warnings, { kind: 'warning' })) { + (0, webpack_diagnostics_1.addWarning)(compilation, warning); + } + } + } +} +exports.CssOptimizerPlugin = CssOptimizerPlugin; diff --git a/src/tools/webpack/plugins/dedupe-module-resolve-plugin.d.ts b/src/tools/webpack/plugins/dedupe-module-resolve-plugin.d.ts new file mode 100644 index 000000000..0ae8e19cd --- /dev/null +++ b/src/tools/webpack/plugins/dedupe-module-resolve-plugin.d.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Compiler } from 'webpack'; +export interface DedupeModuleResolvePluginOptions { + verbose?: boolean; +} +/** + * DedupeModuleResolvePlugin is a webpack plugin which dedupes modules with the same name and versions + * that are laid out in different parts of the node_modules tree. + * + * This is needed because Webpack relies on package managers to hoist modules and doesn't have any deduping logic. + * + * This is similar to how Webpack's 'NormalModuleReplacementPlugin' works + * @see https://github.com/webpack/webpack/blob/4a1f068828c2ab47537d8be30d542cd3a1076db4/lib/NormalModuleReplacementPlugin.js#L9 + */ +export declare class DedupeModuleResolvePlugin { + private options?; + modules: Map; + constructor(options?: DedupeModuleResolvePluginOptions | undefined); + apply(compiler: Compiler): void; +} diff --git a/src/tools/webpack/plugins/dedupe-module-resolve-plugin.js b/src/tools/webpack/plugins/dedupe-module-resolve-plugin.js new file mode 100644 index 000000000..4a8d64c14 --- /dev/null +++ b/src/tools/webpack/plugins/dedupe-module-resolve-plugin.js @@ -0,0 +1,75 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DedupeModuleResolvePlugin = void 0; +const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function getResourceData(resolveData) { + const { descriptionFileData, relativePath } = resolveData.createData.resourceResolveData; + return { + packageName: descriptionFileData?.name, + packageVersion: descriptionFileData?.version, + relativePath, + resource: resolveData.createData.resource, + }; +} +/** + * DedupeModuleResolvePlugin is a webpack plugin which dedupes modules with the same name and versions + * that are laid out in different parts of the node_modules tree. + * + * This is needed because Webpack relies on package managers to hoist modules and doesn't have any deduping logic. + * + * This is similar to how Webpack's 'NormalModuleReplacementPlugin' works + * @see https://github.com/webpack/webpack/blob/4a1f068828c2ab47537d8be30d542cd3a1076db4/lib/NormalModuleReplacementPlugin.js#L9 + */ +class DedupeModuleResolvePlugin { + options; + modules = new Map(); + constructor(options) { + this.options = options; + } + apply(compiler) { + compiler.hooks.compilation.tap('DedupeModuleResolvePlugin', (compilation, { normalModuleFactory }) => { + normalModuleFactory.hooks.afterResolve.tap('DedupeModuleResolvePlugin', (result) => { + if (!result) { + return; + } + const { packageName, packageVersion, relativePath, resource } = getResourceData(result); + // Empty name or versions are no valid primary entrypoints of a library + if (!packageName || !packageVersion) { + return; + } + const moduleId = packageName + '@' + packageVersion + ':' + relativePath; + const prevResolvedModule = this.modules.get(moduleId); + if (!prevResolvedModule) { + // This is the first time we visit this module. + this.modules.set(moduleId, { + resource, + request: result.request, + }); + return; + } + const { resource: prevResource, request: prevRequest } = prevResolvedModule; + if (resource === prevResource) { + // No deduping needed. + // Current path and previously resolved path are the same. + return; + } + if (this.options?.verbose) { + (0, webpack_diagnostics_1.addWarning)(compilation, `[DedupeModuleResolvePlugin]: ${resource} -> ${prevResource}`); + } + // Alter current request with previously resolved module. + const createData = result.createData; + createData.resource = prevResource; + createData.userRequest = prevRequest; + }); + }); + } +} +exports.DedupeModuleResolvePlugin = DedupeModuleResolvePlugin; diff --git a/src/tools/webpack/plugins/devtools-ignore-plugin.d.ts b/src/tools/webpack/plugins/devtools-ignore-plugin.d.ts new file mode 100644 index 000000000..2aca668a3 --- /dev/null +++ b/src/tools/webpack/plugins/devtools-ignore-plugin.d.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Compiler } from 'webpack'; +/** + * This plugin adds a field to source maps that identifies which sources are + * vendored or runtime-injected (aka third-party) sources. These are consumed by + * Chrome DevTools to automatically ignore-list sources. + */ +export declare class DevToolsIgnorePlugin { + apply(compiler: Compiler): void; +} diff --git a/src/tools/webpack/plugins/devtools-ignore-plugin.js b/src/tools/webpack/plugins/devtools-ignore-plugin.js new file mode 100644 index 000000000..e9cf65c98 --- /dev/null +++ b/src/tools/webpack/plugins/devtools-ignore-plugin.js @@ -0,0 +1,56 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DevToolsIgnorePlugin = void 0; +const webpack_1 = require("webpack"); +// Following the naming conventions from +// https://sourcemaps.info/spec.html#h.ghqpj1ytqjbm +const IGNORE_LIST = 'x_google_ignoreList'; +const PLUGIN_NAME = 'devtools-ignore-plugin'; +/** + * This plugin adds a field to source maps that identifies which sources are + * vendored or runtime-injected (aka third-party) sources. These are consumed by + * Chrome DevTools to automatically ignore-list sources. + */ +class DevToolsIgnorePlugin { + apply(compiler) { + const { RawSource } = compiler.webpack.sources; + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.processAssets.tap({ + name: PLUGIN_NAME, + stage: webpack_1.Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING, + additionalAssets: true, + }, (assets) => { + for (const [name, asset] of Object.entries(assets)) { + // Instead of using `asset.map()` to fetch the source maps from + // SourceMapSource assets, process them directly as a RawSource. + // This is because `.map()` is slow and can take several seconds. + if (!name.endsWith('.map')) { + // Ignore non source map files. + continue; + } + const mapContent = asset.source().toString(); + if (!mapContent) { + continue; + } + const map = JSON.parse(mapContent); + const ignoreList = []; + for (const [index, path] of map.sources.entries()) { + if (path.includes('/node_modules/') || path.startsWith('webpack/')) { + ignoreList.push(index); + } + } + map[IGNORE_LIST] = ignoreList; + compilation.updateAsset(name, new RawSource(JSON.stringify(map))); + } + }); + }); + } +} +exports.DevToolsIgnorePlugin = DevToolsIgnorePlugin; diff --git a/src/tools/webpack/plugins/esbuild-executor.d.ts b/src/tools/webpack/plugins/esbuild-executor.d.ts new file mode 100644 index 000000000..bd271f777 --- /dev/null +++ b/src/tools/webpack/plugins/esbuild-executor.d.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { FormatMessagesOptions, PartialMessage, TransformOptions, TransformResult } from 'esbuild'; +/** + * Provides the ability to execute esbuild regardless of the current platform's support + * for using the native variant of esbuild. The native variant will be preferred (assuming + * the `alwaysUseWasm` constructor option is `false) due to its inherent performance advantages. + * At first use of esbuild, a supportability test will be automatically performed and the + * WASM-variant will be used if needed by the platform. + */ +export declare class EsbuildExecutor implements Pick { + private alwaysUseWasm; + private esbuildTransform; + private esbuildFormatMessages; + private initialized; + /** + * Constructs an instance of the `EsbuildExecutor` class. + * + * @param alwaysUseWasm If true, the WASM-variant will be preferred and no support test will be + * performed; if false (default), the native variant will be preferred. + */ + constructor(alwaysUseWasm?: boolean); + /** + * Determines whether the native variant of esbuild can be used on the current platform. + * + * @returns A promise which resolves to `true`, if the native variant of esbuild is support or `false`, if the WASM variant is required. + */ + static hasNativeSupport(): Promise; + /** + * Initializes the esbuild transform and format messages functions. + * + * @returns A promise that fulfills when esbuild has been loaded and available for use. + */ + private ensureEsbuild; + /** + * Transitions an executor instance to use the WASM-variant of esbuild. + */ + private useWasm; + transform(input: string | Uint8Array, options?: TransformOptions): Promise; + formatMessages(messages: PartialMessage[], options: FormatMessagesOptions): Promise; +} diff --git a/src/tools/webpack/plugins/esbuild-executor.js b/src/tools/webpack/plugins/esbuild-executor.js new file mode 100644 index 000000000..60bbf11df --- /dev/null +++ b/src/tools/webpack/plugins/esbuild-executor.js @@ -0,0 +1,133 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.EsbuildExecutor = void 0; +/** + * Provides the ability to execute esbuild regardless of the current platform's support + * for using the native variant of esbuild. The native variant will be preferred (assuming + * the `alwaysUseWasm` constructor option is `false) due to its inherent performance advantages. + * At first use of esbuild, a supportability test will be automatically performed and the + * WASM-variant will be used if needed by the platform. + */ +class EsbuildExecutor { + alwaysUseWasm; + esbuildTransform; + esbuildFormatMessages; + initialized = false; + /** + * Constructs an instance of the `EsbuildExecutor` class. + * + * @param alwaysUseWasm If true, the WASM-variant will be preferred and no support test will be + * performed; if false (default), the native variant will be preferred. + */ + constructor(alwaysUseWasm = false) { + this.alwaysUseWasm = alwaysUseWasm; + this.esbuildTransform = this.esbuildFormatMessages = () => { + throw new Error('esbuild implementation missing'); + }; + } + /** + * Determines whether the native variant of esbuild can be used on the current platform. + * + * @returns A promise which resolves to `true`, if the native variant of esbuild is support or `false`, if the WASM variant is required. + */ + static async hasNativeSupport() { + // Try to use native variant to ensure it is functional for the platform. + try { + const { formatMessages } = await Promise.resolve().then(() => __importStar(require('esbuild'))); + await formatMessages([], { kind: 'error' }); + return true; + } + catch { + return false; + } + } + /** + * Initializes the esbuild transform and format messages functions. + * + * @returns A promise that fulfills when esbuild has been loaded and available for use. + */ + async ensureEsbuild() { + if (this.initialized) { + return; + } + // If the WASM variant was preferred at class construction or native is not supported, use WASM + if (this.alwaysUseWasm || !(await EsbuildExecutor.hasNativeSupport())) { + await this.useWasm(); + this.initialized = true; + return; + } + try { + // Use the faster native variant if available. + const { transform, formatMessages } = await Promise.resolve().then(() => __importStar(require('esbuild'))); + this.esbuildTransform = transform; + this.esbuildFormatMessages = formatMessages; + } + catch { + // If the native variant is not installed then use the WASM-based variant + await this.useWasm(); + } + this.initialized = true; + } + /** + * Transitions an executor instance to use the WASM-variant of esbuild. + */ + async useWasm() { + const { transform, formatMessages } = await Promise.resolve().then(() => __importStar(require('esbuild-wasm'))); + this.esbuildTransform = transform; + this.esbuildFormatMessages = formatMessages; + // The ESBUILD_BINARY_PATH environment variable cannot exist when attempting to use the + // WASM variant. If it is then the binary located at the specified path will be used instead + // of the WASM variant. + delete process.env.ESBUILD_BINARY_PATH; + this.alwaysUseWasm = true; + } + async transform(input, options) { + await this.ensureEsbuild(); + return this.esbuildTransform(input, options); + } + async formatMessages(messages, options) { + await this.ensureEsbuild(); + return this.esbuildFormatMessages(messages, options); + } +} +exports.EsbuildExecutor = EsbuildExecutor; diff --git a/src/tools/webpack/plugins/hmr/hmr-accept.d.ts b/src/tools/webpack/plugins/hmr/hmr-accept.d.ts new file mode 100644 index 000000000..ab227c63f --- /dev/null +++ b/src/tools/webpack/plugins/hmr/hmr-accept.d.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export default function (mod: any): void; diff --git a/src/tools/webpack/plugins/hmr/hmr-accept.js b/src/tools/webpack/plugins/hmr/hmr-accept.js new file mode 100644 index 000000000..489f8ed44 --- /dev/null +++ b/src/tools/webpack/plugins/hmr/hmr-accept.js @@ -0,0 +1,184 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +// eslint-disable-next-line import/no-extraneous-dependencies +const core_1 = require("@angular/core"); +const rxjs_1 = require("rxjs"); +function default_1(mod) { + if (!mod['hot']) { + return; + } + if (!(0, core_1.isDevMode)()) { + console.error(`[NG HMR] Cannot use HMR when Angular is running in production mode. To prevent production mode, do not call 'enableProdMode()'.`); + return; + } + mod['hot'].accept(); + mod['hot'].dispose(() => { + if (typeof ng === 'undefined') { + console.warn(`[NG HMR] Cannot find global 'ng'. Likely this is caused because scripts optimization is enabled.`); + return; + } + if (!ng.getInjector) { + // View Engine + return; + } + // Reset JIT compiled components cache + (0, core_1.ɵresetCompiledComponents)(); + const appRoot = getAppRoot(); + if (!appRoot) { + return; + } + const appRef = getApplicationRef(appRoot); + if (!appRef) { + return; + } + // Inputs that are hidden should be ignored + const oldInputs = document.querySelectorAll('input:not([type="hidden"]), textarea'); + const oldOptions = document.querySelectorAll('option'); + // Create new application + appRef.components.forEach((cp) => { + const element = cp.location.nativeElement; + const parentNode = element.parentNode; + parentNode.insertBefore(document.createElement(element.tagName), element); + parentNode.removeChild(element); + }); + // Destroy old application, injectors, { + observer.disconnect(); + const newAppRoot = getAppRoot(); + if (!newAppRoot) { + return; + } + const newAppRef = getApplicationRef(newAppRoot); + if (!newAppRef) { + return; + } + // Wait until the application isStable to restore the form values + newAppRef.isStable + .pipe((0, rxjs_1.filter)((isStable) => !!isStable), (0, rxjs_1.take)(1)) + .subscribe(() => restoreFormValues(oldInputs, oldOptions)); + }).observe(bodyElement, { + attributes: true, + subtree: true, + attributeFilter: ['ng-version'], + }); + }); +} +function getAppRoot() { + const appRoot = document.querySelector('[ng-version]'); + if (!appRoot) { + console.warn('[NG HMR] Cannot find the application root component.'); + return undefined; + } + return appRoot; +} +function getToken(appRoot, token) { + return (typeof ng === 'object' && ng.getInjector(appRoot).get(token)) || undefined; +} +function getApplicationRef(appRoot) { + const appRef = getToken(appRoot, core_1.ApplicationRef); + if (!appRef) { + console.warn(`[NG HMR] Cannot get 'ApplicationRef'.`); + return undefined; + } + return appRef; +} +function getPlatformRef(appRoot) { + const platformRef = getToken(appRoot, core_1.PlatformRef); + if (!platformRef) { + console.warn(`[NG HMR] Cannot get 'PlatformRef'.`); + return undefined; + } + return platformRef; +} +function dispatchEvents(element) { + element.dispatchEvent(new Event('input', { + bubbles: true, + cancelable: true, + })); + element.blur(); + element.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })); +} +function restoreFormValues(oldInputs, oldOptions) { + // Restore input that are not hidden + const newInputs = document.querySelectorAll('input:not([type="hidden"]), textarea'); + if (newInputs.length && newInputs.length === oldInputs.length) { + console.log('[NG HMR] Restoring input/textarea values.'); + for (let index = 0; index < newInputs.length; index++) { + const newElement = newInputs[index]; + const oldElement = oldInputs[index]; + switch (oldElement.type) { + case 'button': + case 'image': + case 'submit': + case 'reset': + // These types don't need any value change. + continue; + case 'radio': + case 'checkbox': + newElement.checked = oldElement.checked; + break; + case 'color': + case 'date': + case 'datetime-local': + case 'email': + case 'hidden': + case 'month': + case 'number': + case 'password': + case 'range': + case 'search': + case 'tel': + case 'text': + case 'textarea': + case 'time': + case 'url': + case 'week': + newElement.value = oldElement.value; + break; + case 'file': + // Ignored due: Uncaught DOMException: Failed to set the 'value' property on 'HTMLInputElement': + // This input element accepts a filename, which may only be programmatically set to the empty string. + break; + default: + console.warn('[NG HMR] Unknown input type ' + oldElement.type + '.'); + continue; + } + dispatchEvents(newElement); + } + } + else if (oldInputs.length) { + console.warn('[NG HMR] Cannot restore input/textarea values.'); + } + // Restore option + const newOptions = document.querySelectorAll('option'); + if (newOptions.length && newOptions.length === oldOptions.length) { + console.log('[NG HMR] Restoring selected options.'); + for (let index = 0; index < newOptions.length; index++) { + const newElement = newOptions[index]; + newElement.selected = oldOptions[index].selected; + dispatchEvents(newElement); + } + } + else if (oldOptions.length) { + console.warn('[NG HMR] Cannot restore selected options.'); + } +} diff --git a/src/tools/webpack/plugins/hmr/hmr-loader.d.ts b/src/tools/webpack/plugins/hmr/hmr-loader.d.ts new file mode 100644 index 000000000..74ebb5516 --- /dev/null +++ b/src/tools/webpack/plugins/hmr/hmr-loader.d.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export declare const HmrLoader: string; +export default function localizeExtractLoader(this: import('webpack').LoaderContext<{}>, content: string, map: Parameters[1]): void; diff --git a/src/tools/webpack/plugins/hmr/hmr-loader.js b/src/tools/webpack/plugins/hmr/hmr-loader.js new file mode 100644 index 000000000..3565ee1b9 --- /dev/null +++ b/src/tools/webpack/plugins/hmr/hmr-loader.js @@ -0,0 +1,24 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.HmrLoader = void 0; +exports.default = localizeExtractLoader; +const node_path_1 = require("node:path"); +exports.HmrLoader = __filename; +const hmrAcceptPath = (0, node_path_1.join)(__dirname, './hmr-accept.js').replace(/\\/g, '/'); +function localizeExtractLoader(content, map) { + const source = `${content} + + // HMR Accept Code + import ngHmrAccept from '${hmrAcceptPath}'; + ngHmrAccept(module); + `; + this.callback(null, source, map); + return; +} diff --git a/src/tools/webpack/plugins/index-html-webpack-plugin.d.ts b/src/tools/webpack/plugins/index-html-webpack-plugin.d.ts new file mode 100644 index 000000000..fbea9dfcf --- /dev/null +++ b/src/tools/webpack/plugins/index-html-webpack-plugin.d.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { IndexHtmlGenerator, IndexHtmlGeneratorOptions, IndexHtmlGeneratorProcessOptions } from '@angular/build/private'; +import { Compilation, Compiler } from 'webpack'; +export interface IndexHtmlWebpackPluginOptions extends IndexHtmlGeneratorOptions, Omit { +} +export declare class IndexHtmlWebpackPlugin extends IndexHtmlGenerator { + readonly options: IndexHtmlWebpackPluginOptions; + private _compilation; + get compilation(): Compilation; + constructor(options: IndexHtmlWebpackPluginOptions); + apply(compiler: Compiler): void; + readAsset(path: string): Promise; + protected readIndex(path: string): Promise; +} diff --git a/src/tools/webpack/plugins/index-html-webpack-plugin.js b/src/tools/webpack/plugins/index-html-webpack-plugin.js new file mode 100644 index 000000000..24b7510e5 --- /dev/null +++ b/src/tools/webpack/plugins/index-html-webpack-plugin.js @@ -0,0 +1,87 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.IndexHtmlWebpackPlugin = void 0; +const private_1 = require("@angular/build/private"); +const node_path_1 = require("node:path"); +const webpack_1 = require("webpack"); +const error_1 = require("../../../utils/error"); +const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); +const PLUGIN_NAME = 'index-html-webpack-plugin'; +class IndexHtmlWebpackPlugin extends private_1.IndexHtmlGenerator { + options; + _compilation; + get compilation() { + if (this._compilation) { + return this._compilation; + } + throw new Error('compilation is undefined.'); + } + constructor(options) { + super(options); + this.options = options; + } + apply(compiler) { + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { + this._compilation = compilation; + compilation.hooks.processAssets.tapPromise({ + name: PLUGIN_NAME, + stage: webpack_1.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE + 1, + }, callback); + }); + const callback = async (assets) => { + const files = []; + try { + for (const chunk of this.compilation.chunks) { + for (const file of chunk.files) { + // https://github.com/webpack/webpack/blob/1f99ad6367f2b8a6ef17cce0e058f7a67fb7db18/lib/config/defaults.js#L1000 + if (file.endsWith('.hot-update.js') || file.endsWith('.hot-update.mjs')) { + continue; + } + files.push({ + name: chunk.name ?? undefined, + file, + extension: (0, node_path_1.extname)(file), + }); + } + } + const { csrContent: content, warnings, errors, } = await this.process({ + files, + outputPath: (0, node_path_1.dirname)(this.options.outputPath), + baseHref: this.options.baseHref, + lang: this.options.lang, + }); + assets[this.options.outputPath] = new webpack_1.sources.RawSource(content); + warnings.forEach((msg) => (0, webpack_diagnostics_1.addWarning)(this.compilation, msg)); + errors.forEach((msg) => (0, webpack_diagnostics_1.addError)(this.compilation, msg)); + } + catch (error) { + (0, error_1.assertIsError)(error); + (0, webpack_diagnostics_1.addError)(this.compilation, error.message); + } + }; + } + async readAsset(path) { + const data = this.compilation.assets[(0, node_path_1.basename)(path)].source(); + return typeof data === 'string' ? data : data.toString(); + } + async readIndex(path) { + return new Promise((resolve, reject) => { + this.compilation.inputFileSystem.readFile(path, (err, data) => { + if (err) { + reject(err); + return; + } + this.compilation.fileDependencies.add(path); + resolve(data?.toString() ?? ''); + }); + }); + } +} +exports.IndexHtmlWebpackPlugin = IndexHtmlWebpackPlugin; diff --git a/src/tools/webpack/plugins/index.d.ts b/src/tools/webpack/plugins/index.d.ts new file mode 100644 index 000000000..c46c62160 --- /dev/null +++ b/src/tools/webpack/plugins/index.d.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export { AnyComponentStyleBudgetChecker } from './any-component-style-budget-checker'; +export { ScriptsWebpackPlugin, type ScriptsWebpackPluginOptions } from './scripts-webpack-plugin'; +export { SuppressExtractedTextChunksWebpackPlugin } from './suppress-entry-chunks-webpack-plugin'; +export { RemoveHashPlugin, type RemoveHashPluginOptions } from './remove-hash-plugin'; +export { DedupeModuleResolvePlugin } from './dedupe-module-resolve-plugin'; +export { CommonJsUsageWarnPlugin } from './common-js-usage-warn-plugin'; +export { JsonStatsPlugin } from './json-stats-plugin'; +export { JavaScriptOptimizerPlugin } from './javascript-optimizer-plugin'; +export { default as PostcssCliResources, type PostcssCliResourcesOptions, } from './postcss-cli-resources'; diff --git a/src/angular-cli-files/plugins/webpack.js b/src/tools/webpack/plugins/index.js similarity index 62% rename from src/angular-cli-files/plugins/webpack.js rename to src/tools/webpack/plugins/index.js index 005953f62..b192e3f29 100644 --- a/src/angular-cli-files/plugins/webpack.js +++ b/src/tools/webpack/plugins/index.js @@ -1,33 +1,32 @@ "use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -exports.WebpackRollupLoader = void 0; +exports.PostcssCliResources = exports.JavaScriptOptimizerPlugin = exports.JsonStatsPlugin = exports.CommonJsUsageWarnPlugin = exports.DedupeModuleResolvePlugin = exports.RemoveHashPlugin = exports.SuppressExtractedTextChunksWebpackPlugin = exports.ScriptsWebpackPlugin = exports.AnyComponentStyleBudgetChecker = void 0; // Exports the webpack plugins we use internally. var any_component_style_budget_checker_1 = require("./any-component-style-budget-checker"); Object.defineProperty(exports, "AnyComponentStyleBudgetChecker", { enumerable: true, get: function () { return any_component_style_budget_checker_1.AnyComponentStyleBudgetChecker; } }); -var optimize_css_webpack_plugin_1 = require("./optimize-css-webpack-plugin"); -Object.defineProperty(exports, "OptimizeCssWebpackPlugin", { enumerable: true, get: function () { return optimize_css_webpack_plugin_1.OptimizeCssWebpackPlugin; } }); -var bundle_budget_1 = require("./bundle-budget"); -Object.defineProperty(exports, "BundleBudgetPlugin", { enumerable: true, get: function () { return bundle_budget_1.BundleBudgetPlugin; } }); var scripts_webpack_plugin_1 = require("./scripts-webpack-plugin"); Object.defineProperty(exports, "ScriptsWebpackPlugin", { enumerable: true, get: function () { return scripts_webpack_plugin_1.ScriptsWebpackPlugin; } }); var suppress_entry_chunks_webpack_plugin_1 = require("./suppress-entry-chunks-webpack-plugin"); Object.defineProperty(exports, "SuppressExtractedTextChunksWebpackPlugin", { enumerable: true, get: function () { return suppress_entry_chunks_webpack_plugin_1.SuppressExtractedTextChunksWebpackPlugin; } }); var remove_hash_plugin_1 = require("./remove-hash-plugin"); Object.defineProperty(exports, "RemoveHashPlugin", { enumerable: true, get: function () { return remove_hash_plugin_1.RemoveHashPlugin; } }); -var named_chunks_plugin_1 = require("./named-chunks-plugin"); -Object.defineProperty(exports, "NamedLazyChunksPlugin", { enumerable: true, get: function () { return named_chunks_plugin_1.NamedLazyChunksPlugin; } }); var dedupe_module_resolve_plugin_1 = require("./dedupe-module-resolve-plugin"); Object.defineProperty(exports, "DedupeModuleResolvePlugin", { enumerable: true, get: function () { return dedupe_module_resolve_plugin_1.DedupeModuleResolvePlugin; } }); var common_js_usage_warn_plugin_1 = require("./common-js-usage-warn-plugin"); Object.defineProperty(exports, "CommonJsUsageWarnPlugin", { enumerable: true, get: function () { return common_js_usage_warn_plugin_1.CommonJsUsageWarnPlugin; } }); +var json_stats_plugin_1 = require("./json-stats-plugin"); +Object.defineProperty(exports, "JsonStatsPlugin", { enumerable: true, get: function () { return json_stats_plugin_1.JsonStatsPlugin; } }); +var javascript_optimizer_plugin_1 = require("./javascript-optimizer-plugin"); +Object.defineProperty(exports, "JavaScriptOptimizerPlugin", { enumerable: true, get: function () { return javascript_optimizer_plugin_1.JavaScriptOptimizerPlugin; } }); var postcss_cli_resources_1 = require("./postcss-cli-resources"); -Object.defineProperty(exports, "PostcssCliResources", { enumerable: true, get: function () { return postcss_cli_resources_1.default; } }); -const path_1 = require("path"); -exports.WebpackRollupLoader = require.resolve(path_1.join(__dirname, 'webpack-rollup-loader')); +Object.defineProperty(exports, "PostcssCliResources", { enumerable: true, get: function () { return __importDefault(postcss_cli_resources_1).default; } }); diff --git a/src/tools/webpack/plugins/javascript-optimizer-plugin.d.ts b/src/tools/webpack/plugins/javascript-optimizer-plugin.d.ts new file mode 100644 index 000000000..97a18013c --- /dev/null +++ b/src/tools/webpack/plugins/javascript-optimizer-plugin.d.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { Compiler } from 'webpack'; +/** + * The options used to configure the {@link JavaScriptOptimizerPlugin}. + */ +export interface JavaScriptOptimizerOptions { + /** + * Enables advanced optimizations in the underlying JavaScript optimizers. + * This currently increases the `terser` passes to 2 and enables the `pure_getters` + * option for `terser`. + */ + advanced?: boolean; + /** + * An object record of string keys that will be replaced with their respective values when found + * within the code during optimization. + */ + define: Record; + /** + * Enables the generation of a sourcemap during optimization. + * The output sourcemap will be a full sourcemap containing the merge of the input sourcemap and + * all intermediate sourcemaps. + */ + sourcemap?: boolean; + /** + * A list of supported browsers that is used for output code. + */ + supportedBrowsers?: string[]; + /** + * Enables the retention of identifier names and ensures that function and class names are + * present in the output code. + * + * **Note**: in some cases symbols are still renamed to avoid collisions. + */ + keepIdentifierNames: boolean; + /** + * Enables the removal of all license comments from the output code. + */ + removeLicenses?: boolean; +} +/** + * A Webpack plugin that provides JavaScript optimization capabilities. + * + * The plugin uses both `esbuild` and `terser` to provide both fast and highly-optimized + * code output. `esbuild` is used as an initial pass to remove the majority of unused code + * as well as shorten identifiers. `terser` is then used as a secondary pass to apply + * optimizations not yet implemented by `esbuild`. + */ +export declare class JavaScriptOptimizerPlugin { + private options; + private targets; + constructor(options: JavaScriptOptimizerOptions); + apply(compiler: Compiler): void; +} diff --git a/src/tools/webpack/plugins/javascript-optimizer-plugin.js b/src/tools/webpack/plugins/javascript-optimizer-plugin.js new file mode 100644 index 000000000..45ef8263d --- /dev/null +++ b/src/tools/webpack/plugins/javascript-optimizer-plugin.js @@ -0,0 +1,167 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.JavaScriptOptimizerPlugin = void 0; +const private_1 = require("@angular/build/private"); +const piscina_1 = __importDefault(require("piscina")); +const environment_options_1 = require("../../../utils/environment-options"); +const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); +const esbuild_executor_1 = require("./esbuild-executor"); +/** + * The maximum number of Workers that will be created to execute optimize tasks. + */ +const MAX_OPTIMIZE_WORKERS = environment_options_1.maxWorkers; +/** + * The name of the plugin provided to Webpack when tapping Webpack compiler hooks. + */ +const PLUGIN_NAME = 'angular-javascript-optimizer'; +/** + * A Webpack plugin that provides JavaScript optimization capabilities. + * + * The plugin uses both `esbuild` and `terser` to provide both fast and highly-optimized + * code output. `esbuild` is used as an initial pass to remove the majority of unused code + * as well as shorten identifiers. `terser` is then used as a secondary pass to apply + * optimizations not yet implemented by `esbuild`. + */ +class JavaScriptOptimizerPlugin { + options; + targets; + constructor(options) { + this.options = options; + if (options.supportedBrowsers) { + this.targets = (0, private_1.transformSupportedBrowsersToTargets)(options.supportedBrowsers); + } + } + apply(compiler) { + const { OriginalSource, SourceMapSource } = compiler.webpack.sources; + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + const logger = compilation.getLogger('build-angular.JavaScriptOptimizerPlugin'); + compilation.hooks.processAssets.tapPromise({ + name: PLUGIN_NAME, + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, + }, async (compilationAssets) => { + logger.time('optimize js assets'); + const scriptsToOptimize = []; + const cache = compilation.options.cache && compilation.getCache('JavaScriptOptimizerPlugin'); + // Analyze the compilation assets for scripts that require optimization + for (const assetName of Object.keys(compilationAssets)) { + if (!assetName.endsWith('.js')) { + continue; + } + const scriptAsset = compilation.getAsset(assetName); + // Skip assets that have already been optimized or are verbatim copies (project assets) + if (!scriptAsset || scriptAsset.info.minimized || scriptAsset.info.copied) { + continue; + } + const { source: scriptAssetSource, name } = scriptAsset; + let cacheItem; + if (cache) { + const eTag = cache.getLazyHashedEtag(scriptAssetSource); + cacheItem = cache.getItemCache(name, eTag); + const cachedOutput = await cacheItem.getPromise(); + if (cachedOutput) { + logger.debug(`${name} restored from cache`); + compilation.updateAsset(name, cachedOutput.source, (assetInfo) => ({ + ...assetInfo, + minimized: true, + })); + continue; + } + } + const { source, map } = scriptAssetSource.sourceAndMap(); + scriptsToOptimize.push({ + name: scriptAsset.name, + code: typeof source === 'string' ? source : source.toString(), + map, + cacheItem, + }); + } + if (scriptsToOptimize.length === 0) { + return; + } + // Ensure all replacement values are strings which is the expected type for esbuild + let define; + if (this.options.define) { + define = {}; + for (const [key, value] of Object.entries(this.options.define)) { + define[key] = String(value); + } + } + // Setup the options used by all worker tasks + const optimizeOptions = { + sourcemap: this.options.sourcemap, + define, + keepIdentifierNames: this.options.keepIdentifierNames, + target: this.targets, + removeLicenses: this.options.removeLicenses, + advanced: this.options.advanced, + // Perform a single native esbuild support check. + // This removes the need for each worker to perform the check which would + // otherwise require spawning a separate process per worker. + alwaysUseWasm: !(await esbuild_executor_1.EsbuildExecutor.hasNativeSupport()), + }; + // Sort scripts so larger scripts start first - worker pool uses a FIFO queue + scriptsToOptimize.sort((a, b) => a.code.length - b.code.length); + // Initialize the task worker pool + const workerPath = require.resolve('./javascript-optimizer-worker'); + const workerPool = new piscina_1.default({ + filename: workerPath, + maxThreads: MAX_OPTIMIZE_WORKERS, + recordTiming: false, + }); + // Enqueue script optimization tasks and update compilation assets as the tasks complete + try { + const tasks = []; + for (const { name, code, map, cacheItem } of scriptsToOptimize) { + logger.time(`optimize asset: ${name}`); + tasks.push(workerPool + .run({ + asset: { + name, + code, + map, + }, + options: optimizeOptions, + }) + .then(async ({ code, name, map, errors }) => { + if (errors?.length) { + for (const error of errors) { + (0, webpack_diagnostics_1.addError)(compilation, `Optimization error [${name}]: ${error}`); + } + return; + } + const optimizedAsset = map + ? new SourceMapSource(code, name, map) + : new OriginalSource(code, name); + compilation.updateAsset(name, optimizedAsset, (assetInfo) => ({ + ...assetInfo, + minimized: true, + })); + logger.timeEnd(`optimize asset: ${name}`); + return cacheItem?.storePromise({ + source: optimizedAsset, + }); + }, (error) => { + (0, webpack_diagnostics_1.addError)(compilation, `Optimization error [${name}]: ${error.stack || error.message}`); + })); + } + await Promise.all(tasks); + } + finally { + void workerPool.destroy(); + } + logger.timeEnd('optimize js assets'); + }); + }); + } +} +exports.JavaScriptOptimizerPlugin = JavaScriptOptimizerPlugin; diff --git a/src/tools/webpack/plugins/javascript-optimizer-worker.d.ts b/src/tools/webpack/plugins/javascript-optimizer-worker.d.ts new file mode 100644 index 000000000..3f626e9db --- /dev/null +++ b/src/tools/webpack/plugins/javascript-optimizer-worker.d.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { SourceMapInput } from '@ampproject/remapping'; +/** + * The options to use when optimizing. + */ +export interface OptimizeRequestOptions { + /** + * Controls advanced optimizations. + * Currently these are only terser related: + * * terser compress passes are set to 2 + * * terser pure_getters option is enabled + */ + advanced?: boolean; + /** + * Specifies the string tokens that should be replaced with a defined value. + */ + define?: Record; + /** + * Controls whether class, function, and variable names should be left intact + * throughout the output code. + */ + keepIdentifierNames: boolean; + /** + * Controls whether license text is removed from the output code. + * Within the CLI, this option is linked to the license extraction functionality. + */ + removeLicenses?: boolean; + /** + * Controls whether source maps should be generated. + */ + sourcemap?: boolean; + /** + * Specifies the list of supported esbuild targets. + * @see: https://esbuild.github.io/api/#target + */ + target?: string[]; + /** + * Controls whether esbuild should only use the WASM-variant instead of trying to + * use the native variant. Some platforms may not support the native-variant and + * this option allows one support test to be conducted prior to all the workers starting. + */ + alwaysUseWasm: boolean; +} +/** + * A request to optimize JavaScript using the supplied options. + */ +interface OptimizeRequest { + /** + * The options to use when optimizing. + */ + options: OptimizeRequestOptions; + /** + * The JavaScript asset to optimize. + */ + asset: { + /** + * The name of the JavaScript asset (typically the filename). + */ + name: string; + /** + * The source content of the JavaScript asset. + */ + code: string; + /** + * The source map of the JavaScript asset, if available. + * This map is merged with all intermediate source maps during optimization. + */ + map: SourceMapInput; + }; +} +/** + * Handles optimization requests sent from the main thread via the `JavaScriptOptimizerPlugin`. + */ +export default function ({ asset, options }: OptimizeRequest): Promise<{ + name: string; + errors: string[]; + code?: undefined; + map?: undefined; +} | { + name: string; + code: string; + map: import("@ampproject/remapping").SourceMap | undefined; + errors?: undefined; +}>; +export {}; diff --git a/src/tools/webpack/plugins/javascript-optimizer-worker.js b/src/tools/webpack/plugins/javascript-optimizer-worker.js new file mode 100644 index 000000000..1f9a750d9 --- /dev/null +++ b/src/tools/webpack/plugins/javascript-optimizer-worker.js @@ -0,0 +1,134 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +const remapping_1 = __importDefault(require("@ampproject/remapping")); +const terser_1 = require("terser"); +const esbuild_executor_1 = require("./esbuild-executor"); +/** + * The cached esbuild executor. + * This will automatically use the native or WASM version based on platform and availability + * with the native version given priority due to its superior performance. + */ +let esbuild; +/** + * Handles optimization requests sent from the main thread via the `JavaScriptOptimizerPlugin`. + */ +async function default_1({ asset, options }) { + // esbuild is used as a first pass + const esbuildResult = await optimizeWithEsbuild(asset.code, asset.name, options); + if (isEsBuildFailure(esbuildResult)) { + return { + name: asset.name, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + errors: await esbuild.formatMessages(esbuildResult.errors, { kind: 'error' }), + }; + } + // terser is used as a second pass + const terserResult = await optimizeWithTerser(asset.name, esbuildResult.code, options.sourcemap, options.advanced); + // Merge intermediate sourcemaps with input sourcemap if enabled + let fullSourcemap; + if (options.sourcemap) { + const partialSourcemaps = []; + if (esbuildResult.map) { + partialSourcemaps.unshift(JSON.parse(esbuildResult.map)); + } + if (terserResult.map) { + partialSourcemaps.unshift(terserResult.map); + } + if (asset.map) { + partialSourcemaps.push(asset.map); + } + fullSourcemap = (0, remapping_1.default)(partialSourcemaps, () => null); + } + return { name: asset.name, code: terserResult.code, map: fullSourcemap }; +} +/** + * Optimizes a JavaScript asset using esbuild. + * + * @param content The JavaScript asset source content to optimize. + * @param name The name of the JavaScript asset. Used to generate source maps. + * @param options The optimization request options to apply to the content. + * @returns A promise that resolves with the optimized code, source map, and any warnings. + */ +async function optimizeWithEsbuild(content, name, options) { + if (!esbuild) { + esbuild = new esbuild_executor_1.EsbuildExecutor(options.alwaysUseWasm); + } + try { + return await esbuild.transform(content, { + minifyIdentifiers: !options.keepIdentifierNames, + minifySyntax: true, + // NOTE: Disabling whitespace ensures unused pure annotations are kept + minifyWhitespace: false, + pure: ['forwardRef'], + legalComments: options.removeLicenses ? 'none' : 'inline', + sourcefile: name, + sourcemap: options.sourcemap && 'external', + define: options.define, + target: options.target, + }); + } + catch (error) { + if (isEsBuildFailure(error)) { + return error; + } + throw error; + } +} +/** + * Optimizes a JavaScript asset using terser. + * + * @param name The name of the JavaScript asset. Used to generate source maps. + * @param code The JavaScript asset source content to optimize. + * @param sourcemaps If true, generate an output source map for the optimized code. + * @param advanced Controls advanced optimizations. + * @returns A promise that resolves with the optimized code and source map. + */ +async function optimizeWithTerser(name, code, sourcemaps, advanced) { + const result = await (0, terser_1.minify)({ [name]: code }, { + compress: { + passes: advanced ? 2 : 1, + pure_getters: advanced, + }, + // Set to ES2015 to prevent higher level features from being introduced when browserslist + // contains older browsers. The build system requires browsers to support ES2015 at a minimum. + ecma: 2015, + // esbuild in the first pass is used to minify identifiers instead of mangle here + mangle: false, + // esbuild in the first pass is used to minify function names + keep_fnames: true, + format: { + // ASCII output is enabled here as well to prevent terser from converting back to UTF-8 + ascii_only: true, + wrap_func_args: false, + }, + sourceMap: sourcemaps && + { + asObject: true, + // typings don't include asObject option + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }, + }); + if (typeof result.code !== 'string') { + throw new Error('Terser failed for unknown reason.'); + } + return { code: result.code, map: result.map }; +} +/** + * Determines if an unknown value is an esbuild BuildFailure error object thrown by esbuild. + * @param value A potential esbuild BuildFailure error object. + * @returns `true` if the object is determined to be a BuildFailure object; otherwise, `false`. + */ +function isEsBuildFailure(value) { + return !!value && typeof value === 'object' && 'errors' in value && 'warnings' in value; +} diff --git a/src/tools/webpack/plugins/json-stats-plugin.d.ts b/src/tools/webpack/plugins/json-stats-plugin.d.ts new file mode 100644 index 000000000..81df6b47a --- /dev/null +++ b/src/tools/webpack/plugins/json-stats-plugin.d.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Compiler } from 'webpack'; +export declare class JsonStatsPlugin { + private readonly statsOutputPath; + constructor(statsOutputPath: string); + apply(compiler: Compiler): void; +} diff --git a/src/tools/webpack/plugins/json-stats-plugin.js b/src/tools/webpack/plugins/json-stats-plugin.js new file mode 100644 index 000000000..da7574b6e --- /dev/null +++ b/src/tools/webpack/plugins/json-stats-plugin.js @@ -0,0 +1,70 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.JsonStatsPlugin = void 0; +const node_fs_1 = require("node:fs"); +const promises_1 = require("node:fs/promises"); +const node_path_1 = require("node:path"); +const promises_2 = require("node:stream/promises"); +const error_1 = require("../../../utils/error"); +const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); +class JsonStatsPlugin { + statsOutputPath; + constructor(statsOutputPath) { + this.statsOutputPath = statsOutputPath; + } + apply(compiler) { + compiler.hooks.done.tapPromise('angular-json-stats', async (stats) => { + const { stringifyChunked } = await Promise.resolve().then(() => __importStar(require('@discoveryjs/json-ext'))); + const data = stats.toJson('verbose'); + try { + await (0, promises_1.mkdir)((0, node_path_1.dirname)(this.statsOutputPath), { recursive: true }); + await (0, promises_2.pipeline)(stringifyChunked(data), (0, node_fs_1.createWriteStream)(this.statsOutputPath)); + } + catch (error) { + (0, error_1.assertIsError)(error); + (0, webpack_diagnostics_1.addError)(stats.compilation, `Unable to write stats file: ${error.message || 'unknown error'}`); + } + }); + } +} +exports.JsonStatsPlugin = JsonStatsPlugin; diff --git a/src/tools/webpack/plugins/karma/karma-context.html b/src/tools/webpack/plugins/karma/karma-context.html new file mode 100644 index 000000000..64139997b --- /dev/null +++ b/src/tools/webpack/plugins/karma/karma-context.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + %SCRIPTS% + + + + + + diff --git a/src/tools/webpack/plugins/karma/karma-debug.html b/src/tools/webpack/plugins/karma/karma-debug.html new file mode 100644 index 000000000..f348daf64 --- /dev/null +++ b/src/tools/webpack/plugins/karma/karma-debug.html @@ -0,0 +1,46 @@ + + + + + %X_UA_COMPATIBLE% + Karma DEBUG RUNNER + + + + + + + + + + + + + + + + %SCRIPTS% + + + + + + diff --git a/src/angular-cli-files/plugins/karma.d.ts b/src/tools/webpack/plugins/karma/karma.d.ts similarity index 50% rename from src/angular-cli-files/plugins/karma.d.ts rename to src/tools/webpack/plugins/karma/karma.d.ts index 7815ee530..7c738facb 100644 --- a/src/angular-cli-files/plugins/karma.d.ts +++ b/src/tools/webpack/plugins/karma/karma.d.ts @@ -1,8 +1,8 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ export {}; diff --git a/src/tools/webpack/plugins/karma/karma.js b/src/tools/webpack/plugins/karma/karma.js new file mode 100644 index 000000000..f0d13aa15 --- /dev/null +++ b/src/tools/webpack/plugins/karma/karma.js @@ -0,0 +1,281 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const path = __importStar(require("node:path")); +const webpack_1 = __importDefault(require("webpack")); +const webpack_dev_middleware_1 = __importDefault(require("webpack-dev-middleware")); +const stats_1 = require("../../utils/stats"); +const node_1 = require("@angular-devkit/core/node"); +const index_1 = require("../../../../utils/index"); +const node_assert_1 = __importDefault(require("node:assert")); +const KARMA_APPLICATION_PATH = '_karma_webpack_'; +let blocked = []; +let isBlocked = false; +let webpackMiddleware; +let successCb; +let failureCb; +const init = (config, emitter) => { + if (!config.buildWebpack) { + throw new Error(`The '@angular-devkit/build-angular/plugins/karma' karma plugin is meant to` + + ` be used from within Angular CLI and will not work correctly outside of it.`); + } + const options = config.buildWebpack.options; + const logger = config.buildWebpack.logger || (0, node_1.createConsoleLogger)(); + successCb = config.buildWebpack.successCb; + failureCb = config.buildWebpack.failureCb; + // Add a reporter that fixes sourcemap urls. + if ((0, index_1.normalizeSourceMaps)(options.sourceMap).scripts) { + config.reporters.unshift('@angular-devkit/build-angular--sourcemap-reporter'); + // Code taken from https://github.com/tschaub/karma-source-map-support. + // We can't use it directly because we need to add it conditionally in this file, and karma + // frameworks cannot be added dynamically. + const smsPath = path.dirname(require.resolve('source-map-support')); + const ksmsPath = path.dirname(require.resolve('karma-source-map-support')); + config.files.unshift({ + pattern: path.join(smsPath, 'browser-source-map-support.js'), + included: true, + served: true, + watched: false, + }, { pattern: path.join(ksmsPath, 'client.js'), included: true, served: true, watched: false }); + } + config.reporters.unshift('@angular-devkit/build-angular--event-reporter'); + // When using code-coverage, auto-add karma-coverage. + if (options.codeCoverage && + !config.reporters.some((r) => r === 'coverage' || r === 'coverage-istanbul')) { + config.reporters.push('coverage'); + } + // Add webpack config. + const webpackConfig = config.buildWebpack.webpackConfig; + const webpackMiddlewareConfig = { + // Hide webpack output because its noisy. + stats: false, + publicPath: `/${KARMA_APPLICATION_PATH}/`, + }; + // Use existing config if any. + config.webpack = { ...webpackConfig, ...config.webpack }; + config.webpackMiddleware = { ...webpackMiddlewareConfig, ...config.webpackMiddleware }; + // Our custom context and debug files list the webpack bundles directly instead of using + // the karma files array. + config.customContextFile = `${__dirname}/karma-context.html`; + config.customDebugFile = `${__dirname}/karma-debug.html`; + // Add the request blocker and the webpack server fallback. + config.beforeMiddleware = config.beforeMiddleware || []; + config.beforeMiddleware.push('@angular-devkit/build-angular--blocker'); + config.middleware = config.middleware || []; + config.middleware.push('@angular-devkit/build-angular--fallback'); + if (config.singleRun) { + // There's no option to turn off file watching in webpack-dev-server, but + // we can override the file watcher instead. + webpackConfig.plugins.unshift({ + apply: (compiler) => { + compiler.hooks.afterEnvironment.tap('karma', () => { + compiler.watchFileSystem = { watch: () => { } }; + }); + }, + }); + } + // Files need to be served from a custom path for Karma. + webpackConfig.output.path = `/${KARMA_APPLICATION_PATH}/`; + webpackConfig.output.publicPath = `/${KARMA_APPLICATION_PATH}/`; + const compiler = (0, webpack_1.default)(webpackConfig, (error, stats) => { + if (error) { + throw error; + } + if (stats?.hasErrors()) { + // Only generate needed JSON stats and when needed. + const statsJson = stats?.toJson({ + all: false, + children: true, + errors: true, + warnings: true, + }); + logger.error((0, stats_1.statsErrorsToString)(statsJson, { colors: true })); + if (config.singleRun) { + // Notify potential listeners of the compile error. + emitter.emit('load_error'); + } + // Finish Karma run early in case of compilation error. + emitter.emit('run_complete', [], { exitCode: 1 }); + // Emit a failure build event if there are compilation errors. + failureCb(); + } + }); + function handler(callback) { + isBlocked = true; + callback?.(); + } + (0, node_assert_1.default)(compiler, 'Webpack compiler factory did not return a compiler instance.'); + compiler.hooks.invalid.tap('karma', () => handler()); + compiler.hooks.watchRun.tapAsync('karma', (_, callback) => handler(callback)); + compiler.hooks.run.tapAsync('karma', (_, callback) => handler(callback)); + webpackMiddleware = (0, webpack_dev_middleware_1.default)(compiler, webpackMiddlewareConfig); + emitter.on('exit', (done) => { + webpackMiddleware.close(); + compiler.close(() => done()); + }); + function unblock() { + isBlocked = false; + blocked.forEach((cb) => cb()); + blocked = []; + } + let lastCompilationHash; + let isFirstRun = true; + return new Promise((resolve) => { + compiler.hooks.done.tap('karma', (stats) => { + if (isFirstRun) { + // This is needed to block Karma from launching browsers before Webpack writes the assets in memory. + // See the below: + // https://github.com/karma-runner/karma-chrome-launcher/issues/154#issuecomment-986661937 + // https://github.com/angular/angular-cli/issues/22495 + isFirstRun = false; + resolve(); + } + if (stats.hasErrors()) { + lastCompilationHash = undefined; + } + else if (stats.hash != lastCompilationHash) { + // Refresh karma only when there are no webpack errors, and if the compilation changed. + lastCompilationHash = stats.hash; + emitter.refreshFiles(); + } + unblock(); + }); + }); +}; +init.$inject = ['config', 'emitter']; +// Block requests until the Webpack compilation is done. +function requestBlocker() { + return function (_request, _response, next) { + if (isBlocked) { + blocked.push(next); + } + else { + next(); + } + }; +} +// Copied from "karma-jasmine-diff-reporter" source code: +// In case, when multiple reporters are used in conjunction +// with initSourcemapReporter, they both will show repetitive log +// messages when displaying everything that supposed to write to terminal. +// So just suppress any logs from initSourcemapReporter by doing nothing on +// browser log, because it is an utility reporter, +// unless it's alone in the "reporters" option and base reporter is used. +function muteDuplicateReporterLogging(context, config) { + context.writeCommonMsg = () => { }; + const reporterName = '@angular/cli'; + const hasTrailingReporters = config.reporters.slice(-1).pop() !== reporterName; + if (hasTrailingReporters) { + context.writeCommonMsg = () => { }; + } +} +// Emits builder events. +const eventReporter = function (baseReporterDecorator, config) { + baseReporterDecorator(this); + muteDuplicateReporterLogging(this, config); + this.onRunComplete = function (_browsers, results) { + if (results.exitCode === 0) { + successCb(); + } + else { + failureCb(); + } + }; + // avoid duplicate failure message + this.specFailure = () => { }; +}; +eventReporter.$inject = ['baseReporterDecorator', 'config']; +// Strip the server address and webpack scheme (webpack://) from error log. +const sourceMapReporter = function (baseReporterDecorator, config) { + baseReporterDecorator(this); + muteDuplicateReporterLogging(this, config); + const urlRegexp = /https:\/\/fanyv88.com:443\/http\/localhost:\d+\/_karma_webpack_\/(webpack:\/)?/gi; + this.onSpecComplete = function (_browser, result) { + if (!result.success) { + result.log = result.log.map((l) => l.replace(urlRegexp, '')); + } + }; + // avoid duplicate complete message + this.onRunComplete = () => { }; + // avoid duplicate failure message + this.specFailure = () => { }; +}; +sourceMapReporter.$inject = ['baseReporterDecorator', 'config']; +// When a request is not found in the karma server, try looking for it from the webpack server root. +function fallbackMiddleware() { + return function (request, response, next) { + if (webpackMiddleware) { + if (request.url && !new RegExp(`\\/${KARMA_APPLICATION_PATH}\\/.*`).test(request.url)) { + request.url = '/' + KARMA_APPLICATION_PATH + request.url; + } + webpackMiddleware(request, response, () => { + const alwaysServe = [ + `/${KARMA_APPLICATION_PATH}/runtime.js`, + `/${KARMA_APPLICATION_PATH}/polyfills.js`, + `/${KARMA_APPLICATION_PATH}/scripts.js`, + `/${KARMA_APPLICATION_PATH}/styles.css`, + `/${KARMA_APPLICATION_PATH}/vendor.js`, + ]; + if (request.url && alwaysServe.includes(request.url)) { + response.statusCode = 200; + response.end(); + } + else { + next(); + } + }); + } + else { + next(); + } + }; +} +module.exports = { + 'framework:@angular-devkit/build-angular': ['factory', init], + 'reporter:@angular-devkit/build-angular--sourcemap-reporter': ['type', sourceMapReporter], + 'reporter:@angular-devkit/build-angular--event-reporter': ['type', eventReporter], + 'middleware:@angular-devkit/build-angular--blocker': ['factory', requestBlocker], + 'middleware:@angular-devkit/build-angular--fallback': ['factory', fallbackMiddleware], +}; diff --git a/src/tools/webpack/plugins/named-chunks-plugin.d.ts b/src/tools/webpack/plugins/named-chunks-plugin.d.ts new file mode 100644 index 000000000..c8edd0df9 --- /dev/null +++ b/src/tools/webpack/plugins/named-chunks-plugin.d.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Compiler } from 'webpack'; +/** + * Webpack will not populate the chunk `name` property unless `webpackChunkName` magic comment is used. + * This however will also effect the filename which is not desired when using `deterministic` chunkIds. + * This plugin will populate the chunk `name` which is mainly used so that users can set bundle budgets on lazy chunks. + */ +export declare class NamedChunksPlugin { + apply(compiler: Compiler): void; + private generateName; +} diff --git a/src/tools/webpack/plugins/named-chunks-plugin.js b/src/tools/webpack/plugins/named-chunks-plugin.js new file mode 100644 index 000000000..84d975d33 --- /dev/null +++ b/src/tools/webpack/plugins/named-chunks-plugin.js @@ -0,0 +1,58 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.NamedChunksPlugin = void 0; +const webpack_1 = require("webpack"); +// `ImportDependency` is not part of Webpack's depenencies typings. +const ImportDependency = require('webpack/lib/dependencies/ImportDependency'); +const PLUGIN_NAME = 'named-chunks-plugin'; +/** + * Webpack will not populate the chunk `name` property unless `webpackChunkName` magic comment is used. + * This however will also effect the filename which is not desired when using `deterministic` chunkIds. + * This plugin will populate the chunk `name` which is mainly used so that users can set bundle budgets on lazy chunks. + */ +class NamedChunksPlugin { + apply(compiler) { + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.chunkAsset.tap(PLUGIN_NAME, (chunk) => { + if (chunk.name) { + return; + } + if ([...chunk.files.values()].every((f) => f.endsWith('.css'))) { + // If all chunk files are CSS files skip. + // This happens when using `import('./styles.css')` in a lazy loaded module. + return undefined; + } + const name = this.generateName(chunk); + if (name) { + chunk.name = name; + } + }); + }); + } + generateName(chunk) { + for (const group of chunk.groupsIterable) { + const [block] = group.getBlocks(); + if (!(block instanceof webpack_1.AsyncDependenciesBlock)) { + continue; + } + if (block.groupOptions.name) { + // Ignore groups which have been named already. + return undefined; + } + for (const dependency of block.dependencies) { + if (dependency instanceof ImportDependency) { + return webpack_1.Template.toPath(dependency.request); + } + } + } + return undefined; + } +} +exports.NamedChunksPlugin = NamedChunksPlugin; diff --git a/src/tools/webpack/plugins/occurrences-plugin.d.ts b/src/tools/webpack/plugins/occurrences-plugin.d.ts new file mode 100644 index 000000000..6249e32db --- /dev/null +++ b/src/tools/webpack/plugins/occurrences-plugin.d.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Compiler } from 'webpack'; +export interface OccurrencesPluginOptions { + aot?: boolean; + scriptsOptimization?: boolean; +} +export declare class OccurrencesPlugin { + private options; + constructor(options: OccurrencesPluginOptions); + apply(compiler: Compiler): void; + private countOccurrences; +} diff --git a/src/tools/webpack/plugins/occurrences-plugin.js b/src/tools/webpack/plugins/occurrences-plugin.js new file mode 100644 index 000000000..26754b366 --- /dev/null +++ b/src/tools/webpack/plugins/occurrences-plugin.js @@ -0,0 +1,80 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OccurrencesPlugin = void 0; +const PLUGIN_NAME = 'angular-occurrences-plugin'; +class OccurrencesPlugin { + options; + constructor(options) { + this.options = options; + } + apply(compiler) { + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.processAssets.tapPromise({ + name: PLUGIN_NAME, + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ANALYSE, + }, async (compilationAssets) => { + for (const assetName of Object.keys(compilationAssets)) { + if (!assetName.endsWith('.js')) { + continue; + } + const scriptAsset = compilation.getAsset(assetName); + if (!scriptAsset || scriptAsset.source.size() <= 0) { + continue; + } + const src = scriptAsset.source.source().toString('utf-8'); + let ngComponentCount = 0; + if (!this.options.aot) { + // Count the number of `Component({` strings (case sensitive), which happens in __decorate(). + ngComponentCount += this.countOccurrences(src, 'Component({'); + } + if (this.options.scriptsOptimization) { + // for ascii_only true + ngComponentCount += this.countOccurrences(src, '.\\u0275cmp', false); + } + else { + // For Ivy we just count ɵcmp.src + ngComponentCount += this.countOccurrences(src, '.ɵcmp', true); + } + compilation.updateAsset(assetName, (s) => s, (assetInfo) => ({ + ...assetInfo, + ngComponentCount, + })); + } + }); + }); + } + countOccurrences(source, match, wordBreak = false) { + let count = 0; + // We condition here so branch prediction happens out of the loop, not in it. + if (wordBreak) { + const re = /\w/; + for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) { + if (!(re.test(source[pos - 1] || '') || re.test(source[pos + match.length] || ''))) { + count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH! + } + pos -= match.length; + if (pos < 0) { + break; + } + } + } + else { + for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) { + count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH! + pos -= match.length; + if (pos < 0) { + break; + } + } + } + return count; + } +} +exports.OccurrencesPlugin = OccurrencesPlugin; diff --git a/src/tools/webpack/plugins/postcss-cli-resources.d.ts b/src/tools/webpack/plugins/postcss-cli-resources.d.ts new file mode 100644 index 000000000..48645ce92 --- /dev/null +++ b/src/tools/webpack/plugins/postcss-cli-resources.d.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Plugin } from 'postcss'; +export interface PostcssCliResourcesOptions { + baseHref?: string; + deployUrl?: string; + resourcesOutputPath?: string; + rebaseRootRelative?: boolean; + /** CSS is extracted to a `.css` or is embedded in a `.js` file. */ + extracted?: boolean; + filename: (resourcePath: string) => string; + loader: import('webpack').LoaderContext; + emitFile: boolean; +} +export declare const postcss = true; +export default function (options?: PostcssCliResourcesOptions): Plugin; diff --git a/src/angular-cli-files/plugins/postcss-cli-resources.js b/src/tools/webpack/plugins/postcss-cli-resources.js similarity index 53% rename from src/angular-cli-files/plugins/postcss-cli-resources.js rename to src/tools/webpack/plugins/postcss-cli-resources.js index 85080c420..8a20d6eca 100644 --- a/src/angular-cli-files/plugins/postcss-cli-resources.js +++ b/src/tools/webpack/plugins/postcss-cli-resources.js @@ -1,19 +1,54 @@ "use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.postcss = void 0; +exports.default = default_1; const loader_utils_1 = require("loader-utils"); -const path = require("path"); -const postcss = require("postcss"); -const url = require("url"); +const path = __importStar(require("node:path")); +const url = __importStar(require("node:url")); +const error_1 = require("../../../utils/error"); function wrapUrl(url) { let wrappedUrl; - const hasSingleQuotes = url.indexOf('\'') >= 0; + const hasSingleQuotes = url.indexOf("'") >= 0; if (hasSingleQuotes) { wrappedUrl = `"${url}"`; } @@ -26,50 +61,36 @@ async function resolve(file, base, resolver) { try { return await resolver('./' + file, base); } - catch (_a) { + catch { return resolver(file, base); } } -exports.default = postcss.plugin('postcss-cli-resources', (options) => { - const { deployUrl = '', baseHref = '', resourcesOutputPath = '', rebaseRootRelative = false, filename, loader, emitFile, } = options; - const dedupeSlashes = (url) => url.replace(/\/\/+/g, '/'); +exports.postcss = true; +function default_1(options) { + if (!options) { + throw new Error('No options were specified to "postcss-cli-resources".'); + } + const { deployUrl = '', resourcesOutputPath = '', filename, loader, emitFile, extracted, } = options; const process = async (inputUrl, context, resourceCache) => { // If root-relative, absolute or protocol relative url, leave as is if (/^((?:\w+:)?\/\/|data:|chrome:|#)/.test(inputUrl)) { return inputUrl; } - if (!rebaseRootRelative && /^\//.test(inputUrl)) { + if (/^\//.test(inputUrl)) { return inputUrl; } // If starts with a caret, remove and return remainder // this supports bypassing asset processing if (inputUrl.startsWith('^')) { - return inputUrl.substr(1); + return inputUrl.slice(1); } const cacheKey = path.resolve(context, inputUrl); const cachedUrl = resourceCache.get(cacheKey); if (cachedUrl) { return cachedUrl; } - if (rebaseRootRelative && inputUrl.startsWith('/')) { - let outputUrl = ''; - if (deployUrl.match(/:\/\//) || deployUrl.startsWith('/')) { - // If deployUrl is absolute or root relative, ignore baseHref & use deployUrl as is. - outputUrl = `${deployUrl.replace(/\/$/, '')}${inputUrl}`; - } - else if (baseHref.match(/:\/\//)) { - // If baseHref contains a scheme, include it as is. - outputUrl = baseHref.replace(/\/$/, '') + dedupeSlashes(`/${deployUrl}/${inputUrl}`); - } - else { - // Join together base-href, deploy-url and the original URL. - outputUrl = dedupeSlashes(`/${baseHref}/${deployUrl}/${inputUrl}`); - } - resourceCache.set(cacheKey, outputUrl); - return outputUrl; - } if (inputUrl.startsWith('~')) { - inputUrl = inputUrl.substr(1); + inputUrl = inputUrl.slice(1); } const { pathname, hash, search } = url.parse(inputUrl.replace(/\\/g, '/')); const resolver = (file, base) => new Promise((resolve, reject) => { @@ -88,19 +109,26 @@ exports.default = postcss.plugin('postcss-cli-resources', (options) => { reject(err); return; } - let outputPath = loader_utils_1.interpolateName({ resourcePath: result }, filename, { content }); + let outputPath = (0, loader_utils_1.interpolateName)( + // TODO: Revisit. Previously due to lack of type safety, this object + // was fine, but in practice it doesn't match the type of the loader context. + { resourcePath: result }, filename(result), { + content, + context: loader.context || loader.rootContext, + }).replace(/\\|\//g, '-'); if (resourcesOutputPath) { outputPath = path.posix.join(resourcesOutputPath, outputPath); } loader.addDependency(result); if (emitFile) { - loader.emitFile(outputPath, content, undefined); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + loader.emitFile(outputPath, content, undefined, { sourceFilename: result }); } let outputUrl = outputPath.replace(/\\/g, '/'); if (hash || search) { outputUrl = url.format({ pathname: outputUrl, hash, search }); } - if (deployUrl && loader.loaders[loader.loaderIndex].options.ident !== 'extracted') { + if (deployUrl && !extracted) { outputUrl = url.resolve(deployUrl, outputUrl); } resourceCache.set(cacheKey, outputUrl); @@ -108,36 +136,32 @@ exports.default = postcss.plugin('postcss-cli-resources', (options) => { }); }); }; - return (root) => { - const urlDeclarations = []; - root.walkDecls(decl => { - if (decl.value && decl.value.includes('url')) { - urlDeclarations.push(decl); + const resourceCache = new Map(); + const processed = Symbol('postcss-cli-resources'); + return { + postcssPlugin: 'postcss-cli-resources', + async Declaration(decl) { + if (!decl.value.includes('url') || processed in decl) { + return; } - }); - if (urlDeclarations.length === 0) { - return; - } - const resourceCache = new Map(); - return Promise.all(urlDeclarations.map(async (decl) => { const value = decl.value; - const urlRegex = /url\(\s*(?:"([^"]+)"|'([^']+)'|(.+?))\s*\)/g; + const urlRegex = /url(?:\(\s*(['"]?))(.*?)(?:\1\s*\))/g; const segments = []; let match; let lastIndex = 0; let modified = false; // We want to load it relative to the file that imports const inputFile = decl.source && decl.source.input.file; - const context = inputFile && path.dirname(inputFile) || loader.context; - // tslint:disable-next-line:no-conditional-assignment - while (match = urlRegex.exec(value)) { - const originalUrl = match[1] || match[2] || match[3]; + const context = (inputFile && path.dirname(inputFile)) || loader.context; + while ((match = urlRegex.exec(value))) { + const originalUrl = match[2]; let processedUrl; try { processedUrl = await process(originalUrl, context, resourceCache); } catch (err) { - loader.emitError(decl.error(err.message, { word: originalUrl }).toString()); + (0, error_1.assertIsError)(err); + loader.emitError(decl.error(err.message, { word: originalUrl })); continue; } if (lastIndex < match.index) { @@ -158,6 +182,7 @@ exports.default = postcss.plugin('postcss-cli-resources', (options) => { if (modified) { decl.value = segments.join(''); } - })); + decl[processed] = true; + }, }; -}); +} diff --git a/src/tools/webpack/plugins/progress-plugin.d.ts b/src/tools/webpack/plugins/progress-plugin.d.ts new file mode 100644 index 000000000..5d6626d0c --- /dev/null +++ b/src/tools/webpack/plugins/progress-plugin.d.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { ProgressPlugin as WebpackProgressPlugin } from 'webpack'; +export declare class ProgressPlugin extends WebpackProgressPlugin { + constructor(platform: 'server' | 'browser'); +} diff --git a/src/tools/webpack/plugins/progress-plugin.js b/src/tools/webpack/plugins/progress-plugin.js new file mode 100644 index 000000000..1ad7ed6f6 --- /dev/null +++ b/src/tools/webpack/plugins/progress-plugin.js @@ -0,0 +1,38 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ProgressPlugin = void 0; +const webpack_1 = require("webpack"); +const spinner_1 = require("../../../utils/spinner"); +class ProgressPlugin extends webpack_1.ProgressPlugin { + constructor(platform) { + const platformCapitalFirst = platform.replace(/^\w/, (s) => s.toUpperCase()); + const spinner = new spinner_1.Spinner(); + spinner.start(`Generating ${platform} application bundles (phase: setup)...`); + super({ + handler: (percentage, message) => { + const phase = message ? ` (phase: ${message})` : ''; + spinner.text = `Generating ${platform} application bundles${phase}...`; + switch (percentage) { + case 1: + if (spinner.isSpinning) { + spinner.succeed(`${platformCapitalFirst} application bundle generation complete.`); + } + break; + case 0: + if (!spinner.isSpinning) { + spinner.start(); + } + break; + } + }, + }); + } +} +exports.ProgressPlugin = ProgressPlugin; diff --git a/src/angular-cli-files/plugins/remove-hash-plugin.d.ts b/src/tools/webpack/plugins/remove-hash-plugin.d.ts similarity index 69% rename from src/angular-cli-files/plugins/remove-hash-plugin.d.ts rename to src/tools/webpack/plugins/remove-hash-plugin.d.ts index 6751fb8ac..3eedd3e37 100644 --- a/src/angular-cli-files/plugins/remove-hash-plugin.d.ts +++ b/src/tools/webpack/plugins/remove-hash-plugin.d.ts @@ -1,12 +1,12 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Compiler } from 'webpack'; -import { HashFormat } from '../models/webpack-configs/utils'; +import { HashFormat } from '../utils/helpers'; export interface RemoveHashPluginOptions { chunkNames: string[]; hashFormat: HashFormat; diff --git a/src/tools/webpack/plugins/remove-hash-plugin.js b/src/tools/webpack/plugins/remove-hash-plugin.js new file mode 100644 index 000000000..6c01e4b5d --- /dev/null +++ b/src/tools/webpack/plugins/remove-hash-plugin.js @@ -0,0 +1,31 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.RemoveHashPlugin = void 0; +class RemoveHashPlugin { + options; + constructor(options) { + this.options = options; + } + apply(compiler) { + compiler.hooks.compilation.tap('remove-hash-plugin', (compilation) => { + const assetPath = (path, data) => { + const chunkName = data.chunk?.name; + const { chunkNames, hashFormat } = this.options; + if (chunkName && chunkNames?.includes(chunkName)) { + // Replace hash formats with empty strings. + return path.replace(hashFormat.chunk, '').replace(hashFormat.extract, ''); + } + return path; + }; + compilation.hooks.assetPath.tap('remove-hash-plugin', assetPath); + }); + } +} +exports.RemoveHashPlugin = RemoveHashPlugin; diff --git a/src/tools/webpack/plugins/scripts-webpack-plugin.d.ts b/src/tools/webpack/plugins/scripts-webpack-plugin.d.ts new file mode 100644 index 000000000..599cabb37 --- /dev/null +++ b/src/tools/webpack/plugins/scripts-webpack-plugin.d.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { Compilation, Compiler } from 'webpack'; +export interface ScriptsWebpackPluginOptions { + name: string; + sourceMap?: boolean; + scripts: string[]; + filename: string; + basePath: string; +} +export declare class ScriptsWebpackPlugin { + private options; + private _lastBuildTime?; + private _cachedOutput?; + constructor(options: ScriptsWebpackPluginOptions); + shouldSkip(compilation: Compilation, scripts: string[]): Promise; + private _insertOutput; + apply(compiler: Compiler): void; +} diff --git a/src/tools/webpack/plugins/scripts-webpack-plugin.js b/src/tools/webpack/plugins/scripts-webpack-plugin.js new file mode 100644 index 000000000..1116b204b --- /dev/null +++ b/src/tools/webpack/plugins/scripts-webpack-plugin.js @@ -0,0 +1,195 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ScriptsWebpackPlugin = void 0; +const loader_utils_1 = require("loader-utils"); +const path = __importStar(require("node:path")); +const webpack_1 = require("webpack"); +const error_1 = require("../../../utils/error"); +const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); +const Entrypoint = require('webpack/lib/Entrypoint'); +/** + * The name of the plugin provided to Webpack when tapping Webpack compiler hooks. + */ +const PLUGIN_NAME = 'scripts-webpack-plugin'; +function addDependencies(compilation, scripts) { + for (const script of scripts) { + compilation.fileDependencies.add(script); + } +} +class ScriptsWebpackPlugin { + options; + _lastBuildTime; + _cachedOutput; + constructor(options) { + this.options = options; + } + async shouldSkip(compilation, scripts) { + if (this._lastBuildTime == undefined) { + this._lastBuildTime = Date.now(); + return false; + } + for (const script of scripts) { + const scriptTime = await new Promise((resolve, reject) => { + compilation.fileSystemInfo.getFileTimestamp(script, (error, entry) => { + if (error) { + reject(error); + return; + } + resolve(entry && typeof entry !== 'string' ? entry.safeTime : undefined); + }); + }); + if (!scriptTime || scriptTime > this._lastBuildTime) { + this._lastBuildTime = Date.now(); + return false; + } + } + return true; + } + _insertOutput(compilation, { filename, source }, cached = false) { + const chunk = new webpack_1.Chunk(this.options.name); + chunk.rendered = !cached; + chunk.id = this.options.name; + chunk.ids = [chunk.id]; + chunk.files.add(filename); + const entrypoint = new Entrypoint(this.options.name); + entrypoint.pushChunk(chunk); + chunk.addGroup(entrypoint); + compilation.entrypoints.set(this.options.name, entrypoint); + compilation.chunks.add(chunk); + compilation.assets[filename] = source; + compilation.hooks.chunkAsset.call(chunk, filename); + } + apply(compiler) { + if (this.options.scripts.length === 0) { + return; + } + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { + // Use the resolver from the compilation instead of compiler. + // Using the latter will causes a lot of `DescriptionFileUtils.loadDescriptionFile` calls. + // See: https://github.com/angular/angular-cli/issues/24634#issuecomment-1425782668 + const resolver = compilation.resolverFactory.get('normal', { + preferRelative: true, + useSyncFileSystemCalls: true, + // Caching must be disabled because it causes the resolver to become async after a rebuild. + cache: false, + }); + const scripts = []; + for (const script of this.options.scripts) { + try { + const resolvedPath = resolver.resolveSync({}, this.options.basePath, script); + if (resolvedPath) { + scripts.push(resolvedPath); + } + else { + (0, webpack_diagnostics_1.addError)(compilation, `Cannot resolve '${script}'.`); + } + } + catch (error) { + (0, error_1.assertIsError)(error); + (0, webpack_diagnostics_1.addError)(compilation, error.message); + } + } + compilation.hooks.additionalAssets.tapPromise(PLUGIN_NAME, async () => { + if (await this.shouldSkip(compilation, scripts)) { + if (this._cachedOutput) { + this._insertOutput(compilation, this._cachedOutput, true); + } + addDependencies(compilation, scripts); + return; + } + const sourceGetters = scripts.map((fullPath) => { + return new Promise((resolve, reject) => { + compilation.inputFileSystem.readFile(fullPath, (err, data) => { + if (err) { + reject(err); + return; + } + const content = data?.toString() ?? ''; + let source; + if (this.options.sourceMap) { + // TODO: Look for source map file (for '.min' scripts, etc.) + let adjustedPath = fullPath; + if (this.options.basePath) { + adjustedPath = path.relative(this.options.basePath, fullPath); + } + source = new webpack_1.sources.OriginalSource(content, adjustedPath); + } + else { + source = new webpack_1.sources.RawSource(content); + } + resolve(source); + }); + }); + }); + const sources = await Promise.all(sourceGetters); + const concatSource = new webpack_1.sources.ConcatSource(); + sources.forEach((source) => { + concatSource.add(source); + concatSource.add('\n;'); + }); + const combinedSource = new webpack_1.sources.CachedSource(concatSource); + const output = { filename: this.options.filename, source: combinedSource }; + this._insertOutput(compilation, output); + this._cachedOutput = output; + addDependencies(compilation, scripts); + }); + compilation.hooks.processAssets.tapPromise({ + name: PLUGIN_NAME, + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING, + }, async () => { + const assetName = this.options.filename; + const asset = compilation.getAsset(assetName); + if (asset) { + const interpolatedFilename = (0, loader_utils_1.interpolateName)( + // TODO: Revisit. Previously due to lack of type safety, this object + // was fine, but in practice it doesn't match the type of the loader context. + { resourcePath: 'scripts.js' }, assetName, { content: asset.source.source() }); + if (assetName !== interpolatedFilename) { + compilation.renameAsset(assetName, interpolatedFilename); + } + } + }); + }); + } +} +exports.ScriptsWebpackPlugin = ScriptsWebpackPlugin; diff --git a/src/tools/webpack/plugins/service-worker-plugin.d.ts b/src/tools/webpack/plugins/service-worker-plugin.d.ts new file mode 100644 index 000000000..38ca2f0dd --- /dev/null +++ b/src/tools/webpack/plugins/service-worker-plugin.d.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { Compiler } from 'webpack'; +export interface ServiceWorkerPluginOptions { + projectRoot: string; + root: string; + baseHref?: string; + ngswConfigPath?: string; +} +export declare class ServiceWorkerPlugin { + private readonly options; + constructor(options: ServiceWorkerPluginOptions); + apply(compiler: Compiler): void; +} diff --git a/src/tools/webpack/plugins/service-worker-plugin.js b/src/tools/webpack/plugins/service-worker-plugin.js new file mode 100644 index 000000000..768ae36d2 --- /dev/null +++ b/src/tools/webpack/plugins/service-worker-plugin.js @@ -0,0 +1,46 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ServiceWorkerPlugin = void 0; +const private_1 = require("@angular/build/private"); +class ServiceWorkerPlugin { + options; + constructor(options) { + this.options = options; + } + apply(compiler) { + compiler.hooks.done.tapPromise('angular-service-worker', async (stats) => { + if (stats.hasErrors()) { + // Don't generate a service worker if the compilation has errors. + // When there are errors some files will not be emitted which would cause other errors down the line such as readdir failures. + return; + } + const { projectRoot, root, baseHref = '', ngswConfigPath } = this.options; + const { compilation } = stats; + // We use the output path from the compilation instead of build options since during + // localization the output path is modified to a temp directory. + // See: https://github.com/angular/angular-cli/blob/7e64b1537d54fadb650559214fbb12707324cd75/packages/angular_devkit/build_angular/src/utils/i18n-options.ts#L251-L252 + const outputPath = compilation.outputOptions.path; + if (!outputPath) { + throw new Error('Compilation output path cannot be empty.'); + } + try { + await (0, private_1.augmentAppWithServiceWorker)(projectRoot, root, outputPath, baseHref, ngswConfigPath, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + compiler.inputFileSystem.promises, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + compiler.outputFileSystem.promises); + } + catch (error) { + compilation.errors.push(new compilation.compiler.webpack.WebpackError(`Failed to generate service worker - ${error instanceof Error ? error.message : error}`)); + } + }); + } +} +exports.ServiceWorkerPlugin = ServiceWorkerPlugin; diff --git a/src/tools/webpack/plugins/styles-webpack-plugin.d.ts b/src/tools/webpack/plugins/styles-webpack-plugin.d.ts new file mode 100644 index 000000000..cfedfe92e --- /dev/null +++ b/src/tools/webpack/plugins/styles-webpack-plugin.d.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { Compiler } from 'webpack'; +export interface StylesWebpackPluginOptions { + preserveSymlinks?: boolean; + root: string; + entryPoints: Record; +} +export declare class StylesWebpackPlugin { + private readonly options; + private compilation; + constructor(options: StylesWebpackPluginOptions); + apply(compiler: Compiler): void; +} diff --git a/src/tools/webpack/plugins/styles-webpack-plugin.js b/src/tools/webpack/plugins/styles-webpack-plugin.js new file mode 100644 index 000000000..ed81d5d85 --- /dev/null +++ b/src/tools/webpack/plugins/styles-webpack-plugin.js @@ -0,0 +1,73 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.StylesWebpackPlugin = void 0; +const node_assert_1 = __importDefault(require("node:assert")); +const error_1 = require("../../../utils/error"); +const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); +/** + * The name of the plugin provided to Webpack when tapping Webpack compiler hooks. + */ +const PLUGIN_NAME = 'styles-webpack-plugin'; +class StylesWebpackPlugin { + options; + compilation; + constructor(options) { + this.options = options; + } + apply(compiler) { + const { entryPoints, preserveSymlinks, root } = this.options; + const resolver = compiler.resolverFactory.get('global-styles', { + conditionNames: ['sass', 'less', 'style'], + mainFields: ['sass', 'less', 'style', 'main', '...'], + extensions: ['.scss', '.sass', '.less', '.css'], + restrictions: [/\.((le|sa|sc|c)ss)$/i], + preferRelative: true, + useSyncFileSystemCalls: true, + symlinks: !preserveSymlinks, + fileSystem: compiler.inputFileSystem ?? undefined, + }); + const webpackOptions = compiler.options; + compiler.hooks.environment.tap(PLUGIN_NAME, () => { + const entry = typeof webpackOptions.entry === 'function' ? webpackOptions.entry() : webpackOptions.entry; + webpackOptions.entry = async () => { + const entrypoints = await entry; + for (const [bundleName, paths] of Object.entries(entryPoints)) { + entrypoints[bundleName] ??= {}; + const entryImport = (entrypoints[bundleName].import ??= []); + for (const path of paths) { + try { + const resolvedPath = resolver.resolveSync({}, root, path); + if (resolvedPath) { + entryImport.push(`${resolvedPath}?ngGlobalStyle`); + } + else { + (0, node_assert_1.default)(this.compilation, 'Compilation cannot be undefined.'); + (0, webpack_diagnostics_1.addError)(this.compilation, `Cannot resolve '${path}'.`); + } + } + catch (error) { + (0, node_assert_1.default)(this.compilation, 'Compilation cannot be undefined.'); + (0, error_1.assertIsError)(error); + (0, webpack_diagnostics_1.addError)(this.compilation, error.message); + } + } + } + return entrypoints; + }; + }); + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { + this.compilation = compilation; + }); + } +} +exports.StylesWebpackPlugin = StylesWebpackPlugin; diff --git a/src/tools/webpack/plugins/suppress-entry-chunks-webpack-plugin.d.ts b/src/tools/webpack/plugins/suppress-entry-chunks-webpack-plugin.d.ts new file mode 100644 index 000000000..8b4be70c8 --- /dev/null +++ b/src/tools/webpack/plugins/suppress-entry-chunks-webpack-plugin.d.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +/** + * Remove .js files from entry points consisting entirely of stylesheets. + * To be used together with mini-css-extract-plugin. + */ +export declare class SuppressExtractedTextChunksWebpackPlugin { + apply(compiler: import('webpack').Compiler): void; +} diff --git a/src/tools/webpack/plugins/suppress-entry-chunks-webpack-plugin.js b/src/tools/webpack/plugins/suppress-entry-chunks-webpack-plugin.js new file mode 100644 index 000000000..12f3dfc26 --- /dev/null +++ b/src/tools/webpack/plugins/suppress-entry-chunks-webpack-plugin.js @@ -0,0 +1,51 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SuppressExtractedTextChunksWebpackPlugin = void 0; +/** + * Remove .js files from entry points consisting entirely of stylesheets. + * To be used together with mini-css-extract-plugin. + */ +class SuppressExtractedTextChunksWebpackPlugin { + apply(compiler) { + compiler.hooks.compilation.tap('SuppressExtractedTextChunks', (compilation) => { + compilation.hooks.chunkAsset.tap('SuppressExtractedTextChunks', (chunk, filename) => { + // Remove only JavaScript assets + if (!filename.endsWith('.js')) { + return; + } + // Only chunks with a css asset should have JavaScript assets removed + let hasCssFile = false; + for (const file of chunk.files) { + if (file.endsWith('.css')) { + hasCssFile = true; + break; + } + } + if (!hasCssFile) { + return; + } + // Only chunks with all CSS entry dependencies should have JavaScript assets removed + let cssOnly = false; + const entryModules = compilation.chunkGraph.getChunkEntryModulesIterable(chunk); + for (const module of entryModules) { + cssOnly = module.dependencies.every((dependency) => dependency.constructor.name === 'CssDependency'); + if (!cssOnly) { + break; + } + } + if (cssOnly) { + chunk.files.delete(filename); + compilation.deleteAsset(filename); + } + }); + }); + } +} +exports.SuppressExtractedTextChunksWebpackPlugin = SuppressExtractedTextChunksWebpackPlugin; diff --git a/src/angular-cli-files/plugins/named-chunks-plugin.d.ts b/src/tools/webpack/plugins/transfer-size-plugin.d.ts similarity index 56% rename from src/angular-cli-files/plugins/named-chunks-plugin.d.ts rename to src/tools/webpack/plugins/transfer-size-plugin.d.ts index 5276009ee..fb9aeedd5 100644 --- a/src/angular-cli-files/plugins/named-chunks-plugin.d.ts +++ b/src/tools/webpack/plugins/transfer-size-plugin.d.ts @@ -1,12 +1,12 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Compiler } from 'webpack'; -export declare class NamedLazyChunksPlugin { +export declare class TransferSizePlugin { constructor(); apply(compiler: Compiler): void; } diff --git a/src/tools/webpack/plugins/transfer-size-plugin.js b/src/tools/webpack/plugins/transfer-size-plugin.js new file mode 100644 index 000000000..f5017192a --- /dev/null +++ b/src/tools/webpack/plugins/transfer-size-plugin.js @@ -0,0 +1,49 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TransferSizePlugin = void 0; +const node_util_1 = require("node:util"); +const node_zlib_1 = require("node:zlib"); +const webpack_diagnostics_1 = require("../../../utils/webpack-diagnostics"); +const brotliCompressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliCompress); +const PLUGIN_NAME = 'angular-transfer-size-estimator'; +class TransferSizePlugin { + constructor() { } + apply(compiler) { + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.processAssets.tapPromise({ + name: PLUGIN_NAME, + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ANALYSE, + }, async (compilationAssets) => { + const actions = []; + for (const assetName of Object.keys(compilationAssets)) { + if (!assetName.endsWith('.js') && !assetName.endsWith('.css')) { + continue; + } + const scriptAsset = compilation.getAsset(assetName); + if (!scriptAsset || scriptAsset.source.size() <= 0) { + continue; + } + actions.push(brotliCompressAsync(scriptAsset.source.source()) + .then((result) => { + compilation.updateAsset(assetName, (s) => s, (assetInfo) => ({ + ...assetInfo, + estimatedTransferSize: result.length, + })); + }) + .catch((error) => { + (0, webpack_diagnostics_1.addWarning)(compilation, `Unable to calculate estimated transfer size for '${assetName}'. Reason: ${error.message}`); + })); + } + await Promise.all(actions); + }); + }); + } +} +exports.TransferSizePlugin = TransferSizePlugin; diff --git a/src/tools/webpack/plugins/typescript.d.ts b/src/tools/webpack/plugins/typescript.d.ts new file mode 100644 index 000000000..3a2eefcb4 --- /dev/null +++ b/src/tools/webpack/plugins/typescript.d.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { AngularWebpackPlugin } from '@ngtools/webpack'; +import { WebpackConfigOptions } from '../../../utils/build-options'; +export declare function createIvyPlugin(wco: WebpackConfigOptions, aot: boolean, tsconfig: string): AngularWebpackPlugin; diff --git a/src/tools/webpack/plugins/typescript.js b/src/tools/webpack/plugins/typescript.js new file mode 100644 index 000000000..631b430dd --- /dev/null +++ b/src/tools/webpack/plugins/typescript.js @@ -0,0 +1,52 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createIvyPlugin = createIvyPlugin; +const webpack_1 = require("@ngtools/webpack"); +const typescript_1 = require("typescript"); +function createIvyPlugin(wco, aot, tsconfig) { + const { buildOptions, tsConfig } = wco; + const optimize = buildOptions.optimization.scripts; + const compilerOptions = { + sourceMap: buildOptions.sourceMap.scripts, + declaration: false, + declarationMap: false, + // Disable removing of comments as TS is quite aggressive with these and can + // remove important annotations, such as /* @__PURE__ */. + removeComments: false, + }; + if (tsConfig.options.target === undefined || tsConfig.options.target < typescript_1.ScriptTarget.ES2022) { + compilerOptions.target = typescript_1.ScriptTarget.ES2022; + // If 'useDefineForClassFields' is already defined in the users project leave the value as is. + // Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995 + // which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well. + compilerOptions.useDefineForClassFields ??= false; + wco.logger.warn('TypeScript compiler options "target" and "useDefineForClassFields" are set to "ES2022" and ' + + '"false" respectively by the Angular CLI. To control ECMA version and features use the Browserslist configuration. ' + + 'For more information, see https://angular.dev/tools/cli/build#configuring-browser-compatibility\n' + + `NOTE: You can set the "target" to "ES2022" in the project's tsconfig to remove this warning.`); + } + if (buildOptions.preserveSymlinks !== undefined) { + compilerOptions.preserveSymlinks = buildOptions.preserveSymlinks; + } + const fileReplacements = {}; + if (buildOptions.fileReplacements) { + for (const replacement of buildOptions.fileReplacements) { + fileReplacements[replacement.replace] = replacement.with; + } + } + return new webpack_1.AngularWebpackPlugin({ + tsconfig, + compilerOptions, + fileReplacements, + jitMode: !aot, + emitNgModuleScope: !optimize, + inlineStyleFileExtension: buildOptions.inlineStyleLanguage ?? 'css', + }); +} diff --git a/src/tools/webpack/plugins/watch-files-logs-plugin.d.ts b/src/tools/webpack/plugins/watch-files-logs-plugin.d.ts new file mode 100644 index 000000000..5895026f5 --- /dev/null +++ b/src/tools/webpack/plugins/watch-files-logs-plugin.d.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { Compiler } from 'webpack'; +export declare class WatchFilesLogsPlugin { + apply(compiler: Compiler): void; +} diff --git a/src/tools/webpack/plugins/watch-files-logs-plugin.js b/src/tools/webpack/plugins/watch-files-logs-plugin.js new file mode 100644 index 000000000..ec1fc80db --- /dev/null +++ b/src/tools/webpack/plugins/watch-files-logs-plugin.js @@ -0,0 +1,27 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.WatchFilesLogsPlugin = void 0; +const PLUGIN_NAME = 'angular.watch-files-logs-plugin'; +class WatchFilesLogsPlugin { + apply(compiler) { + compiler.hooks.watchRun.tap(PLUGIN_NAME, ({ modifiedFiles, removedFiles }) => { + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + const logger = compilation.getLogger(PLUGIN_NAME); + if (modifiedFiles?.size) { + logger.log(`Modified files:\n${[...modifiedFiles].join('\n')}\n`); + } + if (removedFiles?.size) { + logger.log(`Removed files:\n${[...removedFiles].join('\n')}\n`); + } + }); + }); + } +} +exports.WatchFilesLogsPlugin = WatchFilesLogsPlugin; diff --git a/src/angular-cli-files/utilities/async-chunks.d.ts b/src/tools/webpack/utils/async-chunks.d.ts similarity index 58% rename from src/angular-cli-files/utilities/async-chunks.d.ts rename to src/tools/webpack/utils/async-chunks.d.ts index e23ff128a..f27812876 100644 --- a/src/angular-cli-files/utilities/async-chunks.d.ts +++ b/src/tools/webpack/utils/async-chunks.d.ts @@ -1,16 +1,16 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import * as webpack from 'webpack'; -import { NormalizedEntryPoint } from '../models/webpack-configs'; +import { StatsChunk, StatsCompilation } from 'webpack'; +import { NormalizedEntryPoint } from './helpers'; /** * Webpack stats may incorrectly mark extra entry points `initial` chunks, when * they are actually loaded asynchronously and thus not in the main bundle. This * function finds extra entry points in Webpack stats and corrects this value * whereever necessary. Does not modify {@param webpackStats}. */ -export declare function markAsyncChunksNonInitial(webpackStats: webpack.Stats.ToJsonOutput, extraEntryPoints: NormalizedEntryPoint[]): Exclude; +export declare function markAsyncChunksNonInitial(webpackStats: StatsCompilation, extraEntryPoints: NormalizedEntryPoint[]): StatsChunk[]; diff --git a/src/angular-cli-files/utilities/async-chunks.js b/src/tools/webpack/utils/async-chunks.js similarity index 66% rename from src/angular-cli-files/utilities/async-chunks.js rename to src/tools/webpack/utils/async-chunks.js index 6e0985513..f92e07b35 100644 --- a/src/angular-cli-files/utilities/async-chunks.js +++ b/src/tools/webpack/utils/async-chunks.js @@ -1,6 +1,13 @@ "use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.markAsyncChunksNonInitial = void 0; +exports.markAsyncChunksNonInitial = markAsyncChunksNonInitial; /** * Webpack stats may incorrectly mark extra entry points `initial` chunks, when * they are actually loaded asynchronously and thus not in the main bundle. This @@ -13,8 +20,9 @@ function markAsyncChunksNonInitial(webpackStats, extraEntryPoints) { // to worry about transitive dependencies because extra entry points cannot be // depended upon in Webpack, thus any extra entry point with `inject: false`, // **cannot** be loaded in main bundle. - const asyncEntryPoints = extraEntryPoints.filter((entryPoint) => !entryPoint.inject); - const asyncChunkIds = flatMap(asyncEntryPoints, (entryPoint) => entryPoints[entryPoint.bundleName].chunks); + const asyncChunkIds = extraEntryPoints + .filter((entryPoint) => !entryPoint.inject && entryPoints[entryPoint.bundleName]) + .flatMap((entryPoint) => entryPoints[entryPoint.bundleName].chunks?.filter((n) => n !== 'runtime')); // Find chunks for each ID. const asyncChunks = asyncChunkIds.map((chunkId) => { const chunk = chunks.find((chunk) => chunk.id === chunkId); @@ -22,18 +30,15 @@ function markAsyncChunksNonInitial(webpackStats, extraEntryPoints) { throw new Error(`Failed to find chunk (${chunkId}) in set:\n${JSON.stringify(chunks)}`); } return chunk; - }) - // All Webpack chunks are dependent on `runtime`, which is never an async - // entry point, simply ignore this one. - .filter((chunk) => chunk.names.indexOf('runtime') === -1); + }); // A chunk is considered `initial` only if Webpack already belives it to be initial // and the application developer did not mark it async via an extra entry point. - return chunks.map((chunk) => ({ - ...chunk, - initial: chunk.initial && !asyncChunks.find((asyncChunk) => asyncChunk === chunk), - })); -} -exports.markAsyncChunksNonInitial = markAsyncChunksNonInitial; -function flatMap(list, mapper) { - return [].concat(...list.map(mapper)); + return chunks.map((chunk) => { + return asyncChunks.find((asyncChunk) => asyncChunk === chunk) + ? { + ...chunk, + initial: false, + } + : chunk; + }); } diff --git a/src/tools/webpack/utils/helpers.d.ts b/src/tools/webpack/utils/helpers.d.ts new file mode 100644 index 000000000..4e4b14245 --- /dev/null +++ b/src/tools/webpack/utils/helpers.d.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { ObjectPattern } from 'copy-webpack-plugin'; +import type { Configuration, WebpackOptionsNormalized } from 'webpack'; +import { AssetPatternClass, OutputHashing, ScriptElement, StyleElement } from '../../../builders/browser/schema'; +import { WebpackConfigOptions } from '../../../utils/build-options'; +export interface HashFormat { + chunk: string; + extract: string; + file: string; + script: string; +} +export type WebpackStatsOptions = Exclude; +export declare function getOutputHashFormat(outputHashing?: OutputHashing, length?: number): HashFormat; +export type NormalizedEntryPoint = Required>; +export declare function normalizeExtraEntryPoints(extraEntryPoints: (ScriptElement | StyleElement)[], defaultBundleName: string): NormalizedEntryPoint[]; +export declare function assetNameTemplateFactory(hashFormat: HashFormat): (resourcePath: string) => string; +export declare function getInstrumentationExcludedPaths(root: string, excludedPaths: string[]): Set; +export declare function normalizeGlobalStyles(styleEntrypoints: StyleElement[]): { + entryPoints: Record; + noInjectNames: string[]; +}; +export declare function getCacheSettings(wco: WebpackConfigOptions, angularVersion: string): WebpackOptionsNormalized['cache']; +export declare function globalScriptsByBundleName(scripts: ScriptElement[]): { + bundleName: string; + inject: boolean; + paths: string[]; +}[]; +export declare function assetPatterns(root: string, assets: AssetPatternClass[]): ObjectPattern[]; +export declare function getStatsOptions(verbose?: boolean): WebpackStatsOptions; +/** + * Checks if a specified package is installed in the given workspace. + * + * @param root - The root directory of the workspace. + * @param name - The name of the package to check for. + * @returns `true` if the package is installed, `false` otherwise. + */ +export declare function isPackageInstalled(root: string, name: string): boolean; diff --git a/src/tools/webpack/utils/helpers.js b/src/tools/webpack/utils/helpers.js new file mode 100644 index 000000000..64dd97f97 --- /dev/null +++ b/src/tools/webpack/utils/helpers.js @@ -0,0 +1,300 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getOutputHashFormat = getOutputHashFormat; +exports.normalizeExtraEntryPoints = normalizeExtraEntryPoints; +exports.assetNameTemplateFactory = assetNameTemplateFactory; +exports.getInstrumentationExcludedPaths = getInstrumentationExcludedPaths; +exports.normalizeGlobalStyles = normalizeGlobalStyles; +exports.getCacheSettings = getCacheSettings; +exports.globalScriptsByBundleName = globalScriptsByBundleName; +exports.assetPatterns = assetPatterns; +exports.getStatsOptions = getStatsOptions; +exports.isPackageInstalled = isPackageInstalled; +const node_crypto_1 = require("node:crypto"); +const path = __importStar(require("node:path")); +const tinyglobby_1 = require("tinyglobby"); +const schema_1 = require("../../../builders/browser/schema"); +const package_version_1 = require("../../../utils/package-version"); +function getOutputHashFormat(outputHashing = schema_1.OutputHashing.None, length = 20) { + const hashTemplate = `.[contenthash:${length}]`; + switch (outputHashing) { + case 'media': + return { + chunk: '', + extract: '', + file: hashTemplate, + script: '', + }; + case 'bundles': + return { + chunk: hashTemplate, + extract: hashTemplate, + file: '', + script: hashTemplate, + }; + case 'all': + return { + chunk: hashTemplate, + extract: hashTemplate, + file: hashTemplate, + script: hashTemplate, + }; + case 'none': + default: + return { + chunk: '', + extract: '', + file: '', + script: '', + }; + } +} +function normalizeExtraEntryPoints(extraEntryPoints, defaultBundleName) { + return extraEntryPoints.map((entry) => { + if (typeof entry === 'string') { + return { input: entry, inject: true, bundleName: defaultBundleName }; + } + const { inject = true, ...newEntry } = entry; + let bundleName; + if (entry.bundleName) { + bundleName = entry.bundleName; + } + else if (!inject) { + // Lazy entry points use the file name as bundle name. + bundleName = path.parse(entry.input).name; + } + else { + bundleName = defaultBundleName; + } + return { ...newEntry, inject, bundleName }; + }); +} +function assetNameTemplateFactory(hashFormat) { + const visitedFiles = new Map(); + return (resourcePath) => { + if (hashFormat.file) { + // File names are hashed therefore we don't need to handle files with the same file name. + return `[name]${hashFormat.file}.[ext]`; + } + const filename = path.basename(resourcePath); + // Check if the file with the same name has already been processed. + const visited = visitedFiles.get(filename); + if (!visited) { + // Not visited. + visitedFiles.set(filename, resourcePath); + return filename; + } + else if (visited === resourcePath) { + // Same file. + return filename; + } + // File has the same name but it's in a different location. + return '[path][name].[ext]'; + }; +} +function getInstrumentationExcludedPaths(root, excludedPaths) { + const excluded = new Set(); + for (const excludeGlob of excludedPaths) { + const excludePath = excludeGlob[0] === '/' ? excludeGlob.slice(1) : excludeGlob; + (0, tinyglobby_1.globSync)(excludePath, { cwd: root }).forEach((p) => excluded.add(path.join(root, p))); + } + return excluded; +} +function normalizeGlobalStyles(styleEntrypoints) { + const entryPoints = {}; + const noInjectNames = []; + if (styleEntrypoints.length === 0) { + return { entryPoints, noInjectNames }; + } + for (const style of normalizeExtraEntryPoints(styleEntrypoints, 'styles')) { + // Add style entry points. + entryPoints[style.bundleName] ??= []; + entryPoints[style.bundleName].push(style.input); + // Add non injected styles to the list. + if (!style.inject) { + noInjectNames.push(style.bundleName); + } + } + return { entryPoints, noInjectNames }; +} +function getCacheSettings(wco, angularVersion) { + const { enabled, path: cacheDirectory } = wco.buildOptions.cache; + if (enabled) { + return { + type: 'filesystem', + profile: wco.buildOptions.verbose, + cacheDirectory: path.join(cacheDirectory, 'angular-webpack'), + maxMemoryGenerations: 1, + // We use the versions and build options as the cache name. The Webpack configurations are too + // dynamic and shared among different build types: test, build and serve. + // None of which are "named". + name: (0, node_crypto_1.createHash)('sha1') + .update(angularVersion) + .update(package_version_1.VERSION) + .update(wco.projectRoot) + .update(JSON.stringify(wco.tsConfig)) + .update(JSON.stringify({ + ...wco.buildOptions, + // Needed because outputPath changes on every build when using i18n extraction + // https://github.com/angular/angular-cli/blob/736a5f89deaca85f487b78aec9ff66d4118ceb6a/packages/angular_devkit/build_angular/src/utils/i18n-options.ts#L264-L265 + outputPath: undefined, + })) + .digest('hex'), + }; + } + if (wco.buildOptions.watch) { + return { + type: 'memory', + maxGenerations: 1, + }; + } + return false; +} +function globalScriptsByBundleName(scripts) { + return normalizeExtraEntryPoints(scripts, 'scripts').reduce((prev, curr) => { + const { bundleName, inject, input } = curr; + const existingEntry = prev.find((el) => el.bundleName === bundleName); + if (existingEntry) { + if (existingEntry.inject && !inject) { + // All entries have to be lazy for the bundle to be lazy. + throw new Error(`The ${bundleName} bundle is mixing injected and non-injected scripts.`); + } + existingEntry.paths.push(input); + } + else { + prev.push({ + bundleName, + inject, + paths: [input], + }); + } + return prev; + }, []); +} +function assetPatterns(root, assets) { + return assets.map((asset, index) => { + // Resolve input paths relative to workspace root and add slash at the end. + // eslint-disable-next-line prefer-const + let { input, output = '', ignore = [], glob } = asset; + input = path.resolve(root, input).replace(/\\/g, '/'); + input = input.endsWith('/') ? input : input + '/'; + output = output.endsWith('/') ? output : output + '/'; + if (output.startsWith('..')) { + throw new Error('An asset cannot be written to a location outside of the output path.'); + } + return { + context: input, + // Now we remove starting slash to make Webpack place it from the output root. + to: output.replace(/^\//, ''), + from: glob, + noErrorOnMissing: true, + force: true, + globOptions: { + dot: true, + followSymbolicLinks: !!asset.followSymlinks, + ignore: [ + '.gitkeep', + '**/.DS_Store', + '**/Thumbs.db', + // Negate patterns needs to be absolute because copy-webpack-plugin uses absolute globs which + // causes negate patterns not to match. + // See: https://github.com/webpack-contrib/copy-webpack-plugin/issues/498#issuecomment-639327909 + ...ignore, + ].map((i) => path.posix.join(input, i)), + }, + priority: index, + }; + }); +} +function getStatsOptions(verbose = false) { + const webpackOutputOptions = { + all: false, // Fallback value for stats options when an option is not defined. It has precedence over local webpack defaults. + colors: true, + hash: true, // required by custom stat output + timings: true, // required by custom stat output + chunks: true, // required by custom stat output + builtAt: true, // required by custom stat output + warnings: true, + errors: true, + assets: true, // required by custom stat output + cachedAssets: true, // required for bundle size calculators + // Needed for markAsyncChunksNonInitial. + ids: true, + entrypoints: true, + }; + const verboseWebpackOutputOptions = { + // The verbose output will most likely be piped to a file, so colors just mess it up. + colors: false, + usedExports: true, + optimizationBailout: true, + reasons: true, + children: true, + assets: true, + version: true, + chunkModules: true, + errorDetails: true, + errorStack: true, + moduleTrace: true, + logging: 'verbose', + modulesSpace: Infinity, + }; + return verbose + ? { ...webpackOutputOptions, ...verboseWebpackOutputOptions } + : webpackOutputOptions; +} +/** + * Checks if a specified package is installed in the given workspace. + * + * @param root - The root directory of the workspace. + * @param name - The name of the package to check for. + * @returns `true` if the package is installed, `false` otherwise. + */ +function isPackageInstalled(root, name) { + try { + require.resolve(name, { paths: [root] }); + return true; + } + catch { + return false; + } +} diff --git a/src/tools/webpack/utils/stats.d.ts b/src/tools/webpack/utils/stats.d.ts new file mode 100644 index 000000000..c89fe82d0 --- /dev/null +++ b/src/tools/webpack/utils/stats.d.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { BudgetCalculatorResult } from '@angular/build/private'; +import { WebpackLoggingCallback } from '@angular-devkit/build-webpack'; +import { logging } from '@angular-devkit/core'; +import { Configuration, StatsCompilation } from 'webpack'; +import { Schema as BrowserBuilderOptions } from '../../../builders/browser/schema'; +import { WebpackStatsOptions } from './helpers'; +export declare function statsWarningsToString(json: StatsCompilation, statsConfig: WebpackStatsOptions): string; +export declare function statsErrorsToString(json: StatsCompilation, statsConfig: WebpackStatsOptions): string; +export declare function statsHasErrors(json: StatsCompilation): boolean; +export declare function statsHasWarnings(json: StatsCompilation): boolean; +export declare function createWebpackLoggingCallback(options: BrowserBuilderOptions, logger: logging.LoggerApi): WebpackLoggingCallback; +export interface BuildEventStats { + aot: boolean; + optimization: boolean; + allChunksCount: number; + lazyChunksCount: number; + initialChunksCount: number; + changedChunksCount?: number; + durationInMs: number; + cssSizeInBytes: number; + jsSizeInBytes: number; + ngComponentCount: number; +} +export declare function generateBuildEventStats(webpackStats: StatsCompilation, browserBuilderOptions: BrowserBuilderOptions): BuildEventStats; +export declare function webpackStatsLogger(logger: logging.LoggerApi, json: StatsCompilation, config: Configuration, budgetFailures?: BudgetCalculatorResult[]): void; diff --git a/src/tools/webpack/utils/stats.js b/src/tools/webpack/utils/stats.js new file mode 100644 index 000000000..bb3fe78bd --- /dev/null +++ b/src/tools/webpack/utils/stats.js @@ -0,0 +1,300 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.statsWarningsToString = statsWarningsToString; +exports.statsErrorsToString = statsErrorsToString; +exports.statsHasErrors = statsHasErrors; +exports.statsHasWarnings = statsHasWarnings; +exports.createWebpackLoggingCallback = createWebpackLoggingCallback; +exports.generateBuildEventStats = generateBuildEventStats; +exports.webpackStatsLogger = webpackStatsLogger; +const private_1 = require("@angular/build/private"); +const node_assert_1 = __importDefault(require("node:assert")); +const path = __importStar(require("node:path")); +const utils_1 = require("../../../utils"); +const color_1 = require("../../../utils/color"); +const async_chunks_1 = require("./async-chunks"); +const helpers_1 = require("./helpers"); +function getBuildDuration(webpackStats) { + (0, node_assert_1.default)(webpackStats.builtAt, 'buildAt cannot be undefined'); + (0, node_assert_1.default)(webpackStats.time, 'time cannot be undefined'); + return Date.now() - webpackStats.builtAt + webpackStats.time; +} +function generateBundleStats(info) { + const rawSize = typeof info.rawSize === 'number' ? info.rawSize : '-'; + const estimatedTransferSize = typeof info.estimatedTransferSize === 'number' ? info.estimatedTransferSize : '-'; + const files = info.files + ?.filter((f) => !f.endsWith('.map')) + .map((f) => path.basename(f)) + .join(', ') ?? ''; + const names = info.names?.length ? info.names.join(', ') : '-'; + const initial = !!info.initial; + return { + initial, + stats: [files, names, rawSize, estimatedTransferSize], + }; +} +// We use this cache because we can have multiple builders running in the same process, +// where each builder has different output path. +// Ideally, we should create the logging callback as a factory, but that would need a refactoring. +const runsCache = new Set(); +function statsToString(json, +// eslint-disable-next-line @typescript-eslint/no-explicit-any +statsConfig, budgetFailures) { + if (!json.chunks?.length) { + return ''; + } + const colors = statsConfig.colors; + const rs = (x) => (colors ? color_1.colors.reset(x) : x); + const w = (x) => (colors ? color_1.colors.bold.white(x) : x); + const changedChunksStats = []; + let unchangedChunkNumber = 0; + let hasEstimatedTransferSizes = false; + const isFirstRun = !runsCache.has(json.outputPath || ''); + for (const chunk of json.chunks) { + // During first build we want to display unchanged chunks + // but unchanged cached chunks are always marked as not rendered. + if (!isFirstRun && !chunk.rendered) { + continue; + } + const assets = json.assets?.filter((asset) => chunk.files?.includes(asset.name)); + let rawSize = 0; + let estimatedTransferSize; + if (assets) { + for (const asset of assets) { + if (asset.name.endsWith('.map')) { + continue; + } + rawSize += asset.size; + if (typeof asset.info.estimatedTransferSize === 'number') { + if (estimatedTransferSize === undefined) { + estimatedTransferSize = 0; + hasEstimatedTransferSizes = true; + } + estimatedTransferSize += asset.info.estimatedTransferSize; + } + } + } + changedChunksStats.push(generateBundleStats({ ...chunk, rawSize, estimatedTransferSize })); + } + unchangedChunkNumber = json.chunks.length - changedChunksStats.length; + runsCache.add(json.outputPath || ''); + const statsTable = (0, private_1.generateBuildStatsTable)(changedChunksStats, colors, unchangedChunkNumber === 0, hasEstimatedTransferSizes, budgetFailures); + // In some cases we do things outside of webpack context + // Such us index generation, service worker augmentation etc... + // This will correct the time and include these. + const time = getBuildDuration(json); + return rs(`\n${statsTable}\n\n` + + (unchangedChunkNumber > 0 ? `${unchangedChunkNumber} unchanged chunks\n\n` : '') + + `Build at: ${w(new Date().toISOString())} - Hash: ${w(json.hash || '')} - Time: ${w('' + time)}ms`); +} +function statsWarningsToString(json, statsConfig) { + const colors = statsConfig.colors; + const c = (x) => (colors ? color_1.colors.reset.cyan(x) : x); + const y = (x) => (colors ? color_1.colors.reset.yellow(x) : x); + const yb = (x) => (colors ? color_1.colors.reset.yellowBright(x) : x); + const warnings = json.warnings ? [...json.warnings] : []; + if (json.children) { + warnings.push(...json.children.map((c) => c.warnings ?? []).reduce((a, b) => [...a, ...b], [])); + } + let output = ''; + for (const warning of warnings) { + if (typeof warning === 'string') { + output += yb(`Warning: ${warning}\n\n`); + } + else { + let file = warning.file || warning.moduleName; + // Clean up warning paths + // Ex: ./src/app/styles.scss.webpack[javascript/auto]!=!./node_modules/css-loader/dist/cjs.js.... + // to ./src/app/styles.scss.webpack + if (file && !statsConfig.errorDetails) { + const webpackPathIndex = file.indexOf('.webpack['); + if (webpackPathIndex !== -1) { + file = file.substring(0, webpackPathIndex); + } + } + if (file) { + output += c(file); + if (warning.loc) { + output += ':' + yb(warning.loc); + } + output += ' - '; + } + if (!/^warning/i.test(warning.message)) { + output += y('Warning: '); + } + output += `${warning.message}\n\n`; + } + } + return output ? '\n' + output : output; +} +function statsErrorsToString(json, statsConfig) { + const colors = statsConfig.colors; + const c = (x) => (colors ? color_1.colors.reset.cyan(x) : x); + const yb = (x) => (colors ? color_1.colors.reset.yellowBright(x) : x); + const r = (x) => (colors ? color_1.colors.reset.redBright(x) : x); + const errors = json.errors ? [...json.errors] : []; + if (json.children) { + errors.push(...json.children.map((c) => c?.errors || []).reduce((a, b) => [...a, ...b], [])); + } + let output = ''; + for (const error of errors) { + if (typeof error === 'string') { + output += r(`Error: ${error}\n\n`); + } + else { + let file = error.file || error.moduleName; + // Clean up error paths + // Ex: ./src/app/styles.scss.webpack[javascript/auto]!=!./node_modules/css-loader/dist/cjs.js.... + // to ./src/app/styles.scss.webpack + if (file && !statsConfig.errorDetails) { + const webpackPathIndex = file.indexOf('.webpack['); + if (webpackPathIndex !== -1) { + file = file.substring(0, webpackPathIndex); + } + } + if (file) { + output += c(file); + if (error.loc) { + output += ':' + yb(error.loc); + } + output += ' - '; + } + // In most cases webpack will add stack traces to error messages. + // This below cleans up the error from stacks. + // See: https://github.com/webpack/webpack/issues/15980 + const index = error.message.search(/[\n\s]+at /); + const message = statsConfig.errorStack || index === -1 ? error.message : error.message.substring(0, index); + if (!/^error/i.test(message)) { + output += r('Error: '); + } + output += `${message}\n\n`; + } + } + return output ? '\n' + output : output; +} +function statsHasErrors(json) { + return !!(json.errors?.length || json.children?.some((c) => c.errors?.length)); +} +function statsHasWarnings(json) { + return !!(json.warnings?.length || json.children?.some((c) => c.warnings?.length)); +} +function createWebpackLoggingCallback(options, logger) { + const { verbose = false, scripts = [], styles = [] } = options; + const extraEntryPoints = [ + ...(0, helpers_1.normalizeExtraEntryPoints)(styles, 'styles'), + ...(0, helpers_1.normalizeExtraEntryPoints)(scripts, 'scripts'), + ]; + return (stats, config) => { + if (verbose && config.stats !== false) { + const statsOptions = config.stats === true ? undefined : config.stats; + logger.info(stats.toString(statsOptions)); + } + const rawStats = stats.toJson((0, helpers_1.getStatsOptions)(false)); + const webpackStats = { + ...rawStats, + chunks: (0, async_chunks_1.markAsyncChunksNonInitial)(rawStats, extraEntryPoints), + }; + webpackStatsLogger(logger, webpackStats, config); + }; +} +function generateBuildEventStats(webpackStats, browserBuilderOptions) { + const { chunks = [], assets = [] } = webpackStats; + let jsSizeInBytes = 0; + let cssSizeInBytes = 0; + let initialChunksCount = 0; + let ngComponentCount = 0; + let changedChunksCount = 0; + const allChunksCount = chunks.length; + const isFirstRun = !runsCache.has(webpackStats.outputPath || ''); + const chunkFiles = new Set(); + for (const chunk of chunks) { + if (!isFirstRun && chunk.rendered) { + changedChunksCount++; + } + if (chunk.initial) { + initialChunksCount++; + } + for (const file of chunk.files ?? []) { + chunkFiles.add(file); + } + } + for (const asset of assets) { + if (asset.name.endsWith('.map') || !chunkFiles.has(asset.name)) { + continue; + } + if (asset.name.endsWith('.js')) { + jsSizeInBytes += asset.size; + ngComponentCount += asset.info.ngComponentCount ?? 0; + } + else if (asset.name.endsWith('.css')) { + cssSizeInBytes += asset.size; + } + } + return { + optimization: !!(0, utils_1.normalizeOptimization)(browserBuilderOptions.optimization).scripts, + aot: browserBuilderOptions.aot !== false, + allChunksCount, + lazyChunksCount: allChunksCount - initialChunksCount, + initialChunksCount, + changedChunksCount, + durationInMs: getBuildDuration(webpackStats), + cssSizeInBytes, + jsSizeInBytes, + ngComponentCount, + }; +} +function webpackStatsLogger(logger, json, config, budgetFailures) { + logger.info(statsToString(json, config.stats, budgetFailures)); + if (typeof config.stats !== 'object') { + throw new Error('Invalid Webpack stats configuration.'); + } + if (statsHasWarnings(json)) { + logger.warn(statsWarningsToString(json, config.stats)); + } + if (statsHasErrors(json)) { + logger.error(statsErrorsToString(json, config.stats)); + } +} diff --git a/src/transforms.d.ts b/src/transforms.d.ts index cac7fbdd5..8d936f60c 100644 --- a/src/transforms.d.ts +++ b/src/transforms.d.ts @@ -1,8 +1,8 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -export declare type ExecutionTransformer = (input: T) => T | Promise; +export type ExecutionTransformer = (input: T) => T | Promise; diff --git a/src/transforms.js b/src/transforms.js index c8ad2e549..7c2bf23cd 100644 --- a/src/transforms.js +++ b/src/transforms.js @@ -1,2 +1,9 @@ "use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/tslint/index.d.ts b/src/tslint/index.d.ts deleted file mode 100644 index 1fb2a4e40..000000000 --- a/src/tslint/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { json } from '@angular-devkit/core'; -import { Schema as RealTslintBuilderOptions } from './schema'; -declare type TslintBuilderOptions = RealTslintBuilderOptions & json.JsonObject; -declare const _default: import("@angular-devkit/architect/src/internal").Builder; -export default _default; diff --git a/src/tslint/index.js b/src/tslint/index.js deleted file mode 100644 index 5c84f0290..000000000 --- a/src/tslint/index.js +++ /dev/null @@ -1,185 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const architect_1 = require("@angular-devkit/architect"); -const fs_1 = require("fs"); -const glob = require("glob"); -const minimatch_1 = require("minimatch"); -const path = require("path"); -const strip_bom_1 = require("../angular-cli-files/utilities/strip-bom"); -async function _loadTslint() { - let tslint; - try { - tslint = await Promise.resolve().then(() => require('tslint')); // tslint:disable-line:no-implicit-dependencies - } - catch (_a) { - throw new Error('Unable to find TSLint. Ensure TSLint is installed.'); - } - const version = tslint.Linter.VERSION && tslint.Linter.VERSION.split('.'); - if (!version || version.length < 2 - || (Number(version[0]) === 5 && Number(version[1]) < 5) // 5.5+ - || Number(version[0]) < 5 // 6.0+ - ) { - throw new Error('TSLint must be version 5.5 or higher.'); - } - return tslint; -} -async function _run(options, context) { - const systemRoot = context.workspaceRoot; - process.chdir(context.currentDirectory); - const projectName = (context.target && context.target.project) || ''; - // Print formatter output only for non human-readable formats. - const printInfo = ['prose', 'verbose', 'stylish'].includes(options.format || '') && !options.silent; - context.reportStatus(`Linting ${JSON.stringify(projectName)}...`); - if (printInfo) { - context.logger.info(`Linting ${JSON.stringify(projectName)}...`); - } - if (!options.tsConfig && options.typeCheck) { - throw new Error('A "project" must be specified to enable type checking.'); - } - const projectTslint = await _loadTslint(); - const tslintConfigPath = options.tslintConfig - ? path.resolve(systemRoot, options.tslintConfig) - : null; - const Linter = projectTslint.Linter; - let result = undefined; - if (options.tsConfig) { - const tsConfigs = Array.isArray(options.tsConfig) ? options.tsConfig : [options.tsConfig]; - context.reportProgress(0, tsConfigs.length); - const allPrograms = tsConfigs.map(tsConfig => { - return Linter.createProgram(path.resolve(systemRoot, tsConfig)); - }); - let i = 0; - for (const program of allPrograms) { - const partial = await _lint(projectTslint, systemRoot, tslintConfigPath, options, program, allPrograms); - if (result === undefined) { - result = partial; - } - else { - result.failures = result.failures - .filter(curr => { - return !partial.failures.some(prev => curr.equals(prev)); - }) - .concat(partial.failures); - // we are not doing much with 'errorCount' and 'warningCount' - // apart from checking if they are greater than 0 thus no need to dedupe these. - result.errorCount += partial.errorCount; - result.warningCount += partial.warningCount; - result.fileNames = [...new Set([...result.fileNames, ...partial.fileNames])]; - if (partial.fixes) { - result.fixes = result.fixes ? result.fixes.concat(partial.fixes) : partial.fixes; - } - } - context.reportProgress(++i, allPrograms.length); - } - } - else { - result = await _lint(projectTslint, systemRoot, tslintConfigPath, options); - } - if (result == undefined) { - throw new Error('Invalid lint configuration. Nothing to lint.'); - } - if (!options.silent) { - const Formatter = projectTslint.findFormatter(options.format || ''); - if (!Formatter) { - throw new Error(`Invalid lint format "${options.format}".`); - } - const formatter = new Formatter(); - const output = formatter.format(result.failures, result.fixes, result.fileNames); - if (output.trim()) { - context.logger.info(output); - } - } - if (result.warningCount > 0 && printInfo) { - context.logger.warn('Lint warnings found in the listed files.'); - } - if (result.errorCount > 0 && printInfo) { - context.logger.error('Lint errors found in the listed files.'); - } - if (result.warningCount === 0 && result.errorCount === 0 && printInfo) { - context.logger.info('All files pass linting.'); - } - return { - success: options.force || result.errorCount === 0, - }; -} -exports.default = architect_1.createBuilder(_run); -async function _lint(projectTslint, systemRoot, tslintConfigPath, options, program, allPrograms) { - const Linter = projectTslint.Linter; - const Configuration = projectTslint.Configuration; - const files = getFilesToLint(systemRoot, options, Linter, program); - const lintOptions = { - fix: !!options.fix, - formatter: options.format, - }; - const linter = new Linter(lintOptions, program); - let lastDirectory = undefined; - let configLoad; - const lintedFiles = []; - for (const file of files) { - if (program && allPrograms) { - // If it cannot be found in ANY program, then this is an error. - if (allPrograms.every(p => p.getSourceFile(file) === undefined)) { - throw new Error(`File ${JSON.stringify(file)} is not part of a TypeScript project '${options.tsConfig}'.`); - } - else if (program.getSourceFile(file) === undefined) { - // The file exists in some other programs. We will lint it later (or earlier) in the loop. - continue; - } - } - const contents = getFileContents(file); - // Only check for a new tslint config if the path changes. - const currentDirectory = path.dirname(file); - if (currentDirectory !== lastDirectory) { - configLoad = Configuration.findConfiguration(tslintConfigPath, file); - lastDirectory = currentDirectory; - } - if (configLoad) { - // Give some breathing space to other promises that might be waiting. - await Promise.resolve(); - linter.lint(file, contents, configLoad.results); - lintedFiles.push(file); - } - } - return { - ...linter.getResult(), - fileNames: lintedFiles, - }; -} -function getFilesToLint(root, options, linter, program) { - const ignore = options.exclude; - const files = options.files || []; - if (files.length > 0) { - return files - .map(file => glob.sync(file, { cwd: root, ignore, nodir: true })) - .reduce((prev, curr) => prev.concat(curr), []) - .map(file => path.join(root, file)); - } - if (!program) { - return []; - } - let programFiles = linter.getFileNames(program); - if (ignore && ignore.length > 0) { - // normalize to support ./ paths - const ignoreMatchers = ignore - .map(pattern => new minimatch_1.Minimatch(path.normalize(pattern), { dot: true })); - programFiles = programFiles - .filter(file => !ignoreMatchers.some(matcher => matcher.match(path.relative(root, file)))); - } - return programFiles; -} -function getFileContents(file) { - // NOTE: The tslint CLI checks for and excludes MPEG transport streams; this does not. - try { - return strip_bom_1.stripBom(fs_1.readFileSync(file, 'utf-8')); - } - catch (_a) { - throw new Error(`Could not read file '${file}'.`); - } -} diff --git a/src/tslint/schema.d.ts b/src/tslint/schema.d.ts deleted file mode 100644 index 7dab93861..000000000 --- a/src/tslint/schema.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * TSlint target options for Build Facade. - */ -export interface Schema { - /** - * Files to exclude from linting. - */ - exclude?: string[]; - /** - * Files to include in linting. - */ - files?: string[]; - /** - * Fixes linting errors (may overwrite linted files). - */ - fix?: boolean; - /** - * Succeeds even if there was linting errors. - */ - force?: boolean; - /** - * Output format (prose, json, stylish, verbose, pmd, msbuild, checkstyle, vso, fileslist). - */ - format?: string; - /** - * Show output text. - */ - silent?: boolean; - /** - * The name of the TypeScript configuration file. - */ - tsConfig?: TsConfig; - /** - * The name of the TSLint configuration file. - */ - tslintConfig?: string; - /** - * Controls the type check for linting. - */ - typeCheck?: boolean; -} -/** - * The name of the TypeScript configuration file. - */ -export declare type TsConfig = string[] | string; diff --git a/src/tslint/schema.json b/src/tslint/schema.json deleted file mode 100644 index 64fa97adc..000000000 --- a/src/tslint/schema.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "title": "TSlint Target", - "description": "TSlint target options for Build Facade.", - "type": "object", - "properties": { - "tslintConfig": { - "type": "string", - "description": "The name of the TSLint configuration file." - }, - "tsConfig": { - "description": "The name of the TypeScript configuration file.", - "oneOf": [ - { "type": "string" }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "fix": { - "type": "boolean", - "description": "Fixes linting errors (may overwrite linted files).", - "default": false - }, - "typeCheck": { - "type": "boolean", - "description": "Controls the type check for linting.", - "default": false - }, - "force": { - "type": "boolean", - "description": "Succeeds even if there was linting errors.", - "default": false - }, - "silent": { - "type": "boolean", - "description": "Show output text.", - "default": false - }, - "format": { - "type": "string", - "description": "Output format (prose, json, stylish, verbose, pmd, msbuild, checkstyle, vso, fileslist).", - "default": "stylish", - "anyOf": [ - { - "enum": [ - "checkstyle", - "codeFrame", - "filesList", - "json", - "junit", - "msbuild", - "pmd", - "prose", - "stylish", - "tap", - "verbose", - "vso" - ] - }, - { "minLength": 1 } - ] - }, - "exclude": { - "type": "array", - "description": "Files to exclude from linting.", - "default": [], - "items": { - "type": "string" - } - }, - "files": { - "type": "array", - "description": "Files to include in linting.", - "default": [], - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "required": [] -} \ No newline at end of file diff --git a/src/typings.d.ts b/src/typings.d.ts deleted file mode 100644 index 9a5d06cfa..000000000 --- a/src/typings.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -// Workaround for https://github.com/bazelbuild/rules_nodejs/issues/1033 -// Alternative approach instead of https://github.com/angular/angular/pull/33226 -declare module '@babel/core' { - export * from '@types/babel__core'; -} -declare module '@babel/generator' { - export { default } from '@types/babel__generator'; -} -declare module '@babel/traverse' { - export { default } from '@types/babel__traverse'; -} -declare module '@babel/template' { - export { default } from '@types/babel__template'; -} diff --git a/src/utils/action-cache.d.ts b/src/utils/action-cache.d.ts deleted file mode 100644 index 88c0d6ad8..000000000 --- a/src/utils/action-cache.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/// -import * as fs from 'fs'; -import { ProcessBundleOptions, ProcessBundleResult } from './process-bundle'; -export interface CacheEntry { - path: string; - size: number; - integrity?: string; -} -export declare class BundleActionCache { - private readonly cachePath; - private readonly integrityAlgorithm?; - constructor(cachePath: string, integrityAlgorithm?: string | undefined); - static copyEntryContent(entry: CacheEntry | string, dest: fs.PathLike): void; - generateBaseCacheKey(content: string): string; - generateCacheKeys(action: ProcessBundleOptions): string[]; - getCacheEntries(cacheKeys: (string | undefined)[]): Promise<(CacheEntry | null)[] | false>; - getCachedBundleResult(action: ProcessBundleOptions): Promise; -} diff --git a/src/utils/action-cache.js b/src/utils/action-cache.js deleted file mode 100644 index 40a72b138..000000000 --- a/src/utils/action-cache.js +++ /dev/null @@ -1,150 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BundleActionCache = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const crypto_1 = require("crypto"); -const fs = require("fs"); -const copy_file_1 = require("./copy-file"); -const environment_options_1 = require("./environment-options"); -const cacache = require('cacache'); -const packageVersion = require('../../package.json').version; -class BundleActionCache { - constructor(cachePath, integrityAlgorithm) { - this.cachePath = cachePath; - this.integrityAlgorithm = integrityAlgorithm; - } - static copyEntryContent(entry, dest) { - copy_file_1.copyFile(typeof entry === 'string' ? entry : entry.path, dest); - if (process.platform !== 'win32') { - // The cache writes entries as readonly and when using copyFile the permissions will also be copied. - // See: https://github.com/npm/cacache/blob/073fbe1a9f789ba42d9a41de7b8429c93cf61579/lib/util/move-file.js#L36 - fs.chmodSync(dest, 0o644); - } - } - generateBaseCacheKey(content) { - // Create base cache key with elements: - // * package version - different build-angular versions cause different final outputs - // * code length/hash - ensure cached version matches the same input code - const algorithm = this.integrityAlgorithm || 'sha1'; - const codeHash = crypto_1.createHash(algorithm) - .update(content) - .digest('base64'); - let baseCacheKey = `${packageVersion}|${content.length}|${algorithm}-${codeHash}`; - if (!environment_options_1.allowMangle) { - baseCacheKey += '|MD'; - } - return baseCacheKey; - } - generateCacheKeys(action) { - // Postfix added to sourcemap cache keys when vendor, hidden sourcemaps are present - // Allows non-destructive caching of both variants - const sourceMapVendorPostfix = action.sourceMaps && action.vendorSourceMaps ? '|vendor' : ''; - // sourceMappingURL is added at the very end which causes the code to be the same when sourcemaps are enabled/disabled - // When using hiddenSourceMaps we can omit the postfix since sourceMappingURL will not be added. - // When having sourcemaps a hashed file and non hashed file can have the same content. But the sourceMappingURL will differ. - const sourceMapPostFix = action.sourceMaps && !action.hiddenSourceMaps ? `|sourcemap|${action.filename}` : ''; - const baseCacheKey = this.generateBaseCacheKey(action.code); - // Determine cache entries required based on build settings - const cacheKeys = []; - // If optimizing and the original is not ignored, add original as required - if (!action.ignoreOriginal) { - cacheKeys[0 /* OriginalCode */] = baseCacheKey + sourceMapPostFix + '|orig'; - // If sourcemaps are enabled, add original sourcemap as required - if (action.sourceMaps) { - cacheKeys[1 /* OriginalMap */] = baseCacheKey + sourceMapVendorPostfix + '|orig-map'; - } - } - // If not only optimizing, add downlevel as required - if (!action.optimizeOnly) { - cacheKeys[2 /* DownlevelCode */] = baseCacheKey + sourceMapPostFix + '|dl'; - // If sourcemaps are enabled, add downlevel sourcemap as required - if (action.sourceMaps) { - cacheKeys[3 /* DownlevelMap */] = baseCacheKey + sourceMapVendorPostfix + '|dl-map'; - } - } - return cacheKeys; - } - async getCacheEntries(cacheKeys) { - // Attempt to get required cache entries - const cacheEntries = []; - for (const key of cacheKeys) { - if (key) { - const entry = await cacache.get.info(this.cachePath, key); - if (!entry) { - return false; - } - cacheEntries.push({ - path: entry.path, - size: entry.size, - integrity: entry.metadata && entry.metadata.integrity, - }); - } - else { - cacheEntries.push(null); - } - } - return cacheEntries; - } - async getCachedBundleResult(action) { - const entries = action.cacheKeys && await this.getCacheEntries(action.cacheKeys); - if (!entries) { - return null; - } - const result = { name: action.name }; - let cacheEntry = entries[0 /* OriginalCode */]; - if (cacheEntry) { - result.original = { - filename: action.filename, - size: cacheEntry.size, - integrity: cacheEntry.integrity, - }; - BundleActionCache.copyEntryContent(cacheEntry, result.original.filename); - cacheEntry = entries[1 /* OriginalMap */]; - if (cacheEntry) { - result.original.map = { - filename: action.filename + '.map', - size: cacheEntry.size, - }; - BundleActionCache.copyEntryContent(cacheEntry, result.original.filename + '.map'); - } - } - else if (!action.ignoreOriginal) { - // If the original wasn't processed (and therefore not cached), add info - result.original = { - filename: action.filename, - size: Buffer.byteLength(action.code, 'utf8'), - map: action.map === undefined - ? undefined - : { - filename: action.filename + '.map', - size: Buffer.byteLength(action.map, 'utf8'), - }, - }; - } - cacheEntry = entries[2 /* DownlevelCode */]; - if (cacheEntry) { - result.downlevel = { - filename: action.filename.replace(/\-(es20\d{2}|esnext)/, '-es5'), - size: cacheEntry.size, - integrity: cacheEntry.integrity, - }; - BundleActionCache.copyEntryContent(cacheEntry, result.downlevel.filename); - cacheEntry = entries[3 /* DownlevelMap */]; - if (cacheEntry) { - result.downlevel.map = { - filename: action.filename.replace(/\-(es20\d{2}|esnext)/, '-es5') + '.map', - size: cacheEntry.size, - }; - BundleActionCache.copyEntryContent(cacheEntry, result.downlevel.filename + '.map'); - } - } - return result; - } -} -exports.BundleActionCache = BundleActionCache; diff --git a/src/utils/action-executor.d.ts b/src/utils/action-executor.d.ts index a8b059a98..db5479fb0 100644 --- a/src/utils/action-executor.d.ts +++ b/src/utils/action-executor.d.ts @@ -1,21 +1,19 @@ -import { I18nOptions } from './i18n-options'; -import { InlineOptions, ProcessBundleOptions, ProcessBundleResult } from './process-bundle'; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { InlineOptions } from './bundle-inline-options'; +import { I18nOptions } from './i18n-webpack'; export declare class BundleActionExecutor { private workerOptions; - private readonly sizeThreshold; - private largeWorker?; - private smallWorker?; - private cache?; + private workerPool?; constructor(workerOptions: { - cachePath?: string; i18n: I18nOptions; - }, integrityAlgorithm?: string, sizeThreshold?: number); - private static executeMethod; - private ensureLarge; - private ensureSmall; - private executeAction; - process(action: ProcessBundleOptions): Promise; - processAll(actions: Iterable): AsyncIterable; + }); + private ensureWorkerPool; inline(action: InlineOptions): Promise<{ file: string; diagnostics: { diff --git a/src/utils/action-executor.js b/src/utils/action-executor.js index b6811ea2a..fdf1bb8f4 100644 --- a/src/utils/action-executor.js +++ b/src/utils/action-executor.js @@ -1,128 +1,59 @@ "use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BundleActionExecutor = void 0; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -const jest_worker_1 = require("jest-worker"); -const os = require("os"); -const path = require("path"); -const v8 = require("v8"); -const action_cache_1 = require("./action-cache"); -const workers_1 = require("./workers"); -const hasThreadSupport = (() => { - try { - require('worker_threads'); - return true; - } - catch (_a) { - return false; - } -})(); -// This is used to normalize serialization messaging across threads and processes -// Threads use the structured clone algorithm which handles more types -// Processes use JSON which is much more limited -const serialize = v8.serialize; -let workerFile = require.resolve('./process-bundle'); -workerFile = - path.extname(workerFile) === '.ts' - ? require.resolve('./process-bundle-bootstrap') - : workerFile; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BundleActionExecutor = void 0; +const piscina_1 = __importDefault(require("piscina")); +const environment_options_1 = require("./environment-options"); +const workerFile = require.resolve('./process-bundle'); class BundleActionExecutor { - constructor(workerOptions, integrityAlgorithm, sizeThreshold = 32 * 1024) { + workerOptions; + workerPool; + constructor(workerOptions) { this.workerOptions = workerOptions; - this.sizeThreshold = sizeThreshold; - if (workerOptions.cachePath) { - this.cache = new action_cache_1.BundleActionCache(workerOptions.cachePath, integrityAlgorithm); - } } - static executeMethod(worker, method, input) { - return worker[method](input); - } - ensureLarge() { - if (this.largeWorker) { - return this.largeWorker; + ensureWorkerPool() { + if (this.workerPool) { + return this.workerPool; } - // larger files are processed in a separate process to limit memory usage in the main process - return (this.largeWorker = new jest_worker_1.default(workerFile, { - exposedMethods: ['process', 'inlineLocales'], - setupArgs: [[...serialize(this.workerOptions)]], - numWorkers: workers_1.maxWorkers, - })); - } - ensureSmall() { - if (this.smallWorker) { - return this.smallWorker; - } - // small files are processed in a limited number of threads to improve speed - // The limited number also prevents a large increase in memory usage for an otherwise short operation - return (this.smallWorker = new jest_worker_1.default(workerFile, { - exposedMethods: ['process', 'inlineLocales'], - setupArgs: hasThreadSupport ? [this.workerOptions] : [[...serialize(this.workerOptions)]], - numWorkers: os.cpus().length < 2 ? 1 : 2, - enableWorkerThreads: hasThreadSupport, - })); - } - executeAction(method, action) { - // code.length is not an exact byte count but close enough for this - if (action.code.length > this.sizeThreshold) { - return BundleActionExecutor.executeMethod(this.ensureLarge(), method, action); - } - else { - return BundleActionExecutor.executeMethod(this.ensureSmall(), method, action); - } - } - async process(action) { - if (this.cache) { - const cacheKeys = this.cache.generateCacheKeys(action); - action.cacheKeys = cacheKeys; - // Try to get cached data, if it fails fallback to processing - try { - const cachedResult = await this.cache.getCachedBundleResult(action); - if (cachedResult) { - return cachedResult; - } - } - catch (_a) { } - } - return this.executeAction('process', action); - } - processAll(actions) { - return BundleActionExecutor.executeAll(actions, action => this.process(action)); + this.workerPool = new piscina_1.default({ + filename: workerFile, + name: 'inlineLocales', + workerData: this.workerOptions, + maxThreads: environment_options_1.maxWorkers, + recordTiming: false, + }); + return this.workerPool; } async inline(action) { - return this.executeAction('inlineLocales', action); + return this.ensureWorkerPool().run(action, { name: 'inlineLocales' }); } inlineAll(actions) { - return BundleActionExecutor.executeAll(actions, action => this.inline(action)); + return BundleActionExecutor.executeAll(actions, (action) => this.inline(action)); } static async *executeAll(actions, executor) { const executions = new Map(); for (const action of actions) { const execution = executor(action); - executions.set(execution, execution.then(result => { - executions.delete(execution); - return result; - })); + executions.set(execution, execution.then((result) => [execution, result])); } while (executions.size > 0) { - yield Promise.race(executions.values()); + const [execution, result] = await Promise.race(executions.values()); + executions.delete(execution); + yield result; } } stop() { - // Floating promises are intentional here - // https://github.com/facebook/jest/tree/56079a5aceacf32333089cea50c64385885fee26/packages/jest-worker#end - if (this.largeWorker) { - // tslint:disable-next-line: no-floating-promises - this.largeWorker.end(); - } - if (this.smallWorker) { - // tslint:disable-next-line: no-floating-promises - this.smallWorker.end(); + if (this.workerPool) { + void this.workerPool.destroy(); } } } diff --git a/src/utils/build-browser-features.d.ts b/src/utils/build-browser-features.d.ts deleted file mode 100644 index 45a0f72d4..000000000 --- a/src/utils/build-browser-features.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as ts from 'typescript'; -export declare class BuildBrowserFeatures { - private projectRoot; - private scriptTarget; - private readonly _es6TargetOrLater; - readonly supportedBrowsers: string[]; - constructor(projectRoot: string, scriptTarget: ts.ScriptTarget); - /** - * True, when one or more browsers requires ES5 - * support and the scirpt target is ES2015 or greater. - */ - isDifferentialLoadingNeeded(): boolean; - /** - * True, when one or more browsers requires ES5 support - */ - isEs5SupportNeeded(): boolean; - /** - * True, when a browser feature is supported partially or fully. - */ - isFeatureSupported(featureId: string): boolean; -} diff --git a/src/utils/build-browser-features.js b/src/utils/build-browser-features.js deleted file mode 100644 index 75fb0e379..000000000 --- a/src/utils/build-browser-features.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BuildBrowserFeatures = void 0; -const browserslist = require("browserslist"); -const caniuse_lite_1 = require("caniuse-lite"); -const ts = require("typescript"); -class BuildBrowserFeatures { - constructor(projectRoot, scriptTarget) { - this.projectRoot = projectRoot; - this.scriptTarget = scriptTarget; - this.supportedBrowsers = browserslist(undefined, { path: this.projectRoot }); - this._es6TargetOrLater = this.scriptTarget > ts.ScriptTarget.ES5; - } - /** - * True, when one or more browsers requires ES5 - * support and the scirpt target is ES2015 or greater. - */ - isDifferentialLoadingNeeded() { - return this._es6TargetOrLater && this.isEs5SupportNeeded(); - } - /** - * True, when one or more browsers requires ES5 support - */ - isEs5SupportNeeded() { - return !this.isFeatureSupported('es6-module'); - } - /** - * True, when a browser feature is supported partially or fully. - */ - isFeatureSupported(featureId) { - // y: feature is fully available - // n: feature is unavailable - // a: feature is partially supported - // x: feature is prefixed - const criteria = [ - 'y', - 'a', - ]; - const data = caniuse_lite_1.feature(caniuse_lite_1.features[featureId]); - return !this.supportedBrowsers - .some(browser => { - const [agentId, version] = browser.split(' '); - const browserData = data.stats[agentId]; - const featureStatus = (browserData && browserData[version]); - // We are only interested in the first character - // Ex: when 'a #4 #5', we only need to check for 'a' - // as for such cases we should polyfill these features as needed - return !featureStatus || !criteria.includes(featureStatus.charAt(0)); - }); - } -} -exports.BuildBrowserFeatures = BuildBrowserFeatures; diff --git a/src/utils/build-options.d.ts b/src/utils/build-options.d.ts new file mode 100644 index 000000000..b867766e8 --- /dev/null +++ b/src/utils/build-options.d.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { ParsedConfiguration } from '@angular/compiler-cli'; +import { logging } from '@angular-devkit/core'; +import { AssetPatternClass, Budget, CrossOrigin, I18NTranslation, IndexUnion, InlineStyleLanguage, Localize, OutputHashing, ScriptElement, SourceMapClass, StyleElement } from '../builders/browser/schema'; +import { Schema as DevServerSchema } from '../builders/dev-server/schema'; +import { NormalizedCachedOptions } from './normalize-cache'; +import { NormalizedFileReplacement } from './normalize-file-replacements'; +import { NormalizedOptimizationOptions } from './normalize-optimization'; +export interface BuildOptions { + optimization: NormalizedOptimizationOptions; + environment?: string; + outputPath: string; + resourcesOutputPath?: string; + aot?: boolean; + sourceMap: SourceMapClass; + vendorChunk?: boolean; + commonChunk?: boolean; + baseHref?: string; + deployUrl?: string; + verbose?: boolean; + progress?: boolean; + localize?: Localize; + i18nMissingTranslation?: I18NTranslation; + externalDependencies?: string[]; + watch?: boolean; + outputHashing?: OutputHashing; + poll?: number; + index?: IndexUnion; + deleteOutputPath?: boolean; + preserveSymlinks?: boolean; + extractLicenses?: boolean; + buildOptimizer?: boolean; + namedChunks?: boolean; + crossOrigin?: CrossOrigin; + subresourceIntegrity?: boolean; + serviceWorker?: boolean; + webWorkerTsConfig?: string; + statsJson: boolean; + hmr?: boolean; + main: string; + polyfills: string[]; + budgets: Budget[]; + assets: AssetPatternClass[]; + scripts: ScriptElement[]; + styles: StyleElement[]; + stylePreprocessorOptions?: { + includePaths: string[]; + }; + platform?: 'browser' | 'server'; + fileReplacements: NormalizedFileReplacement[]; + inlineStyleLanguage?: InlineStyleLanguage; + allowedCommonJsDependencies?: string[]; + cache: NormalizedCachedOptions; + codeCoverage?: boolean; + codeCoverageExclude?: string[]; + supportedBrowsers?: string[]; +} +export interface WebpackDevServerOptions extends BuildOptions, Omit { +} +export interface WebpackConfigOptions { + root: string; + logger: logging.Logger; + projectRoot: string; + sourceRoot?: string; + buildOptions: T; + tsConfig: ParsedConfiguration; + tsConfigPath: string; + projectName: string; +} diff --git a/src/angular-cli-files/models/build-options.js b/src/utils/build-options.js similarity index 62% rename from src/angular-cli-files/models/build-options.js rename to src/utils/build-options.js index ec6e90c72..7c2bf23cd 100644 --- a/src/angular-cli-files/models/build-options.js +++ b/src/utils/build-options.js @@ -1,9 +1,9 @@ "use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/utils/bundle-inline-options.d.ts b/src/utils/bundle-inline-options.d.ts new file mode 100644 index 000000000..f58023199 --- /dev/null +++ b/src/utils/bundle-inline-options.d.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export interface InlineOptions { + filename: string; + code: string; + map?: string; + outputPath: string; + missingTranslation?: 'warning' | 'error' | 'ignore'; + setLocale?: boolean; +} diff --git a/src/utils/bundle-inline-options.js b/src/utils/bundle-inline-options.js new file mode 100644 index 000000000..7c2bf23cd --- /dev/null +++ b/src/utils/bundle-inline-options.js @@ -0,0 +1,9 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/utils/cache-path.d.ts b/src/utils/cache-path.d.ts deleted file mode 100644 index 7fcb0c26a..000000000 --- a/src/utils/cache-path.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare function findCachePath(name: string): string; diff --git a/src/utils/cache-path.js b/src/utils/cache-path.js deleted file mode 100644 index be5702b2c..000000000 --- a/src/utils/cache-path.js +++ /dev/null @@ -1,21 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.findCachePath = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const findCacheDirectory = require("find-cache-dir"); -const os_1 = require("os"); -const path_1 = require("path"); -const environment_options_1 = require("./environment-options"); -function findCachePath(name) { - if (environment_options_1.cachingBasePath) { - return path_1.resolve(environment_options_1.cachingBasePath, name); - } - return findCacheDirectory({ name }) || os_1.tmpdir(); -} -exports.findCachePath = findCachePath; diff --git a/src/utils/color.d.ts b/src/utils/color.d.ts new file mode 100644 index 000000000..340725320 --- /dev/null +++ b/src/utils/color.d.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import * as ansiColors from 'ansi-colors'; +declare const colors: typeof ansiColors; +export { colors }; diff --git a/src/utils/color.js b/src/utils/color.js new file mode 100644 index 000000000..f50701a9b --- /dev/null +++ b/src/utils/color.js @@ -0,0 +1,73 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.colors = void 0; +const ansiColors = __importStar(require("ansi-colors")); +const node_tty_1 = require("node:tty"); +function supportColor() { + if (process.env.FORCE_COLOR !== undefined) { + // 2 colors: FORCE_COLOR = 0 (Disables colors), depth 1 + // 16 colors: FORCE_COLOR = 1, depth 4 + // 256 colors: FORCE_COLOR = 2, depth 8 + // 16,777,216 colors: FORCE_COLOR = 3, depth 16 + // See: https://nodejs.org/dist/latest-v12.x/docs/api/tty.html#tty_writestream_getcolordepth_env + // and https://github.com/nodejs/node/blob/b9f36062d7b5c5039498e98d2f2c180dca2a7065/lib/internal/tty.js#L106; + switch (process.env.FORCE_COLOR) { + case '': + case 'true': + case '1': + case '2': + case '3': + return true; + default: + return false; + } + } + if (process.stdout instanceof node_tty_1.WriteStream) { + return process.stdout.hasColors(); + } + return false; +} +// Create a separate instance to prevent unintended global changes to the color configuration +const colors = ansiColors.create(); +exports.colors = colors; +colors.enabled = supportColor(); diff --git a/src/utils/copy-assets.d.ts b/src/utils/copy-assets.d.ts index c2f6ee2a4..a67aedf30 100644 --- a/src/utils/copy-assets.d.ts +++ b/src/utils/copy-assets.d.ts @@ -1,7 +1,18 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ export declare function copyAssets(entries: { glob: string; ignore?: string[]; input: string; output: string; flatten?: boolean; -}[], basePaths: Iterable, root: string, changed?: Set): Promise; + followSymlinks?: boolean; +}[], basePaths: Iterable, root: string, changed?: Set): Promise<{ + source: string; + destination: string; +}[]>; diff --git a/src/utils/copy-assets.js b/src/utils/copy-assets.js index 879e72082..b787eead5 100644 --- a/src/utils/copy-assets.js +++ b/src/utils/copy-assets.js @@ -1,49 +1,50 @@ "use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.copyAssets = void 0; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -const fs = require("fs"); -const glob = require("glob"); -const path = require("path"); -const copy_file_1 = require("./copy-file"); -function globAsync(pattern, options) { - return new Promise((resolve, reject) => glob(pattern, options, (e, m) => (e ? reject(e) : resolve(m)))); -} +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.copyAssets = copyAssets; +const node_fs_1 = __importDefault(require("node:fs")); +const node_path_1 = __importDefault(require("node:path")); +const tinyglobby_1 = require("tinyglobby"); async function copyAssets(entries, basePaths, root, changed) { const defaultIgnore = ['.gitkeep', '**/.DS_Store', '**/Thumbs.db']; + const outputFiles = []; for (const entry of entries) { - const cwd = path.resolve(root, entry.input); - const files = await globAsync(entry.glob, { + const cwd = node_path_1.default.resolve(root, entry.input); + const files = await (0, tinyglobby_1.glob)(entry.glob, { cwd, dot: true, - nodir: true, ignore: entry.ignore ? defaultIgnore.concat(entry.ignore) : defaultIgnore, + followSymbolicLinks: entry.followSymlinks, }); const directoryExists = new Set(); for (const file of files) { - const src = path.join(cwd, file); + const src = node_path_1.default.join(cwd, file); if (changed && !changed.has(src)) { continue; } - const filePath = entry.flatten ? path.basename(file) : file; + const filePath = entry.flatten ? node_path_1.default.basename(file) : file; + outputFiles.push({ source: src, destination: node_path_1.default.join(entry.output, filePath) }); for (const base of basePaths) { - const dest = path.join(base, entry.output, filePath); - const dir = path.dirname(dest); + const dest = node_path_1.default.join(base, entry.output, filePath); + const dir = node_path_1.default.dirname(dest); if (!directoryExists.has(dir)) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); + if (!node_fs_1.default.existsSync(dir)) { + node_fs_1.default.mkdirSync(dir, { recursive: true }); } directoryExists.add(dir); } - copy_file_1.copyFile(src, dest); + node_fs_1.default.copyFileSync(src, dest, node_fs_1.default.constants.COPYFILE_FICLONE); } } } + return outputFiles; } -exports.copyAssets = copyAssets; diff --git a/src/utils/copy-file.d.ts b/src/utils/copy-file.d.ts deleted file mode 100644 index ef684dea5..000000000 --- a/src/utils/copy-file.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as fs from 'fs'; -export declare function copyFile(src: fs.PathLike, dest: fs.PathLike): void; diff --git a/src/utils/copy-file.js b/src/utils/copy-file.js deleted file mode 100644 index bad316c70..000000000 --- a/src/utils/copy-file.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.copyFile = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const fs = require("fs"); -// Workaround Node.js issue prior to 10.16 with copyFile on macOS -// https://github.com/angular/angular-cli/issues/15544 & https://github.com/nodejs/node/pull/27241 -let copyFileWorkaround = false; -if (process.platform === 'darwin') { - const version = process.versions.node.split('.').map(part => Number(part)); - if (version[0] < 10 || version[0] === 11 || (version[0] === 10 && version[1] < 16)) { - copyFileWorkaround = true; - } -} -function copyFile(src, dest) { - if (copyFileWorkaround) { - try { - fs.unlinkSync(dest); - } - catch (_a) { } - } - fs.copyFileSync(src, dest, fs.constants.COPYFILE_FICLONE); -} -exports.copyFile = copyFile; diff --git a/src/utils/default-progress.d.ts b/src/utils/default-progress.d.ts index bc06c2a14..64af05050 100644 --- a/src/utils/default-progress.d.ts +++ b/src/utils/default-progress.d.ts @@ -1,8 +1,8 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ export declare function defaultProgress(progress: boolean | undefined): boolean; diff --git a/src/utils/default-progress.js b/src/utils/default-progress.js index 61a753645..9135f93df 100644 --- a/src/utils/default-progress.js +++ b/src/utils/default-progress.js @@ -1,17 +1,16 @@ "use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.defaultProgress = void 0; +exports.defaultProgress = defaultProgress; function defaultProgress(progress) { if (progress === undefined) { return process.stdout.isTTY === true; } return progress; } -exports.defaultProgress = defaultProgress; diff --git a/src/utils/delete-output-dir.d.ts b/src/utils/delete-output-dir.d.ts deleted file mode 100644 index 5ecb579b7..000000000 --- a/src/utils/delete-output-dir.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Delete an output directory, but error out if it's the root of the project. - */ -export declare function deleteOutputDir(root: string, outputPath: string): void; diff --git a/src/utils/delete-output-dir.js b/src/utils/delete-output-dir.js deleted file mode 100644 index 64176cefb..000000000 --- a/src/utils/delete-output-dir.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.deleteOutputDir = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const path_1 = require("path"); -const rimraf = require("rimraf"); -/** - * Delete an output directory, but error out if it's the root of the project. - */ -function deleteOutputDir(root, outputPath) { - const resolvedOutputPath = path_1.resolve(root, outputPath); - if (resolvedOutputPath === root) { - throw new Error('Output path MUST not be project root directory!'); - } - rimraf.sync(resolvedOutputPath); -} -exports.deleteOutputDir = deleteOutputDir; diff --git a/src/utils/empty.js b/src/utils/empty.js deleted file mode 100644 index 8b1378917..000000000 --- a/src/utils/empty.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/utils/environment-options.d.ts b/src/utils/environment-options.d.ts index 331fe5cc0..37d3d1af0 100644 --- a/src/utils/environment-options.d.ts +++ b/src/utils/environment-options.d.ts @@ -1,6 +1,16 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ export declare const allowMangle: boolean; export declare const shouldBeautify: boolean; export declare const allowMinify: boolean; -export declare const cachingDisabled: boolean; -export declare const cachingBasePath: string | null; -export declare const profilingEnabled: boolean; +export declare const maxWorkers: number; +export declare const useParallelTs: boolean; +export declare const debugPerformance: boolean; +export declare const shouldWatchRoot: boolean; +export declare const useTypeChecking: boolean; +export declare const useJSONBuildLogs: boolean; diff --git a/src/utils/environment-options.js b/src/utils/environment-options.js index 5cac14b6c..bce6118d5 100644 --- a/src/utils/environment-options.js +++ b/src/utils/environment-options.js @@ -1,14 +1,14 @@ "use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.profilingEnabled = exports.cachingBasePath = exports.cachingDisabled = exports.allowMinify = exports.shouldBeautify = exports.allowMangle = void 0; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -const path = require("path"); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useJSONBuildLogs = exports.useTypeChecking = exports.shouldWatchRoot = exports.debugPerformance = exports.useParallelTs = exports.maxWorkers = exports.allowMinify = exports.shouldBeautify = exports.allowMangle = void 0; +const node_os_1 = require("node:os"); function isDisabled(variable) { return variable === '0' || variable.toLowerCase() === 'false'; } @@ -57,18 +57,26 @@ exports.allowMangle = isPresent(mangleVariable) : debugOptimize.mangle; exports.shouldBeautify = debugOptimize.beautify; exports.allowMinify = debugOptimize.minify; -// Build cache -const cacheVariable = process.env['NG_BUILD_CACHE']; -exports.cachingDisabled = isPresent(cacheVariable) && isDisabled(cacheVariable); -exports.cachingBasePath = (() => { - if (exports.cachingDisabled || !isPresent(cacheVariable) || isEnabled(cacheVariable)) { - return null; - } - if (!path.isAbsolute(cacheVariable)) { - throw new Error('NG_BUILD_CACHE path value must be absolute.'); - } - return cacheVariable; -})(); -// Build profiling -const profilingVariable = process.env['NG_BUILD_PROFILING']; -exports.profilingEnabled = isPresent(profilingVariable) && isEnabled(profilingVariable); +/** + * Some environments, like CircleCI which use Docker report a number of CPUs by the host and not the count of available. + * This cause `Error: Call retries were exceeded` errors when trying to use them. + * + * @see https://github.com/nodejs/node/issues/28762 + * @see https://github.com/webpack-contrib/terser-webpack-plugin/issues/143 + * @see https://ithub.com/angular/angular-cli/issues/16860#issuecomment-588828079 + * + */ +const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS']; +exports.maxWorkers = isPresent(maxWorkersVariable) + ? +maxWorkersVariable + : Math.min(4, Math.max((0, node_os_1.availableParallelism)() - 1, 1)); +const parallelTsVariable = process.env['NG_BUILD_PARALLEL_TS']; +exports.useParallelTs = !isPresent(parallelTsVariable) || !isDisabled(parallelTsVariable); +const debugPerfVariable = process.env['NG_BUILD_DEBUG_PERF']; +exports.debugPerformance = isPresent(debugPerfVariable) && isEnabled(debugPerfVariable); +const watchRootVariable = process.env['NG_BUILD_WATCH_ROOT']; +exports.shouldWatchRoot = isPresent(watchRootVariable) && isEnabled(watchRootVariable); +const typeCheckingVariable = process.env['NG_BUILD_TYPE_CHECK']; +exports.useTypeChecking = !isPresent(typeCheckingVariable) || !isDisabled(typeCheckingVariable); +const buildLogsJsonVariable = process.env['NG_BUILD_LOGS_JSON']; +exports.useJSONBuildLogs = isPresent(buildLogsJsonVariable) && isEnabled(buildLogsJsonVariable); diff --git a/src/utils/error.d.ts b/src/utils/error.d.ts new file mode 100644 index 000000000..5f481463e --- /dev/null +++ b/src/utils/error.d.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export declare function assertIsError(value: unknown): asserts value is Error & { + code?: string; +}; diff --git a/src/utils/error.js b/src/utils/error.js new file mode 100644 index 000000000..7e1032126 --- /dev/null +++ b/src/utils/error.js @@ -0,0 +1,20 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.assertIsError = assertIsError; +const node_assert_1 = __importDefault(require("node:assert")); +function assertIsError(value) { + const isError = value instanceof Error || + // The following is needing to identify errors coming from RxJs. + (typeof value === 'object' && value && 'name' in value && 'message' in value); + (0, node_assert_1.default)(isError, 'catch clause variable is not an Error instance'); +} diff --git a/src/utils/i18n-inlining.d.ts b/src/utils/i18n-inlining.d.ts index d296fcdc0..63cd80946 100644 --- a/src/utils/i18n-inlining.d.ts +++ b/src/utils/i18n-inlining.d.ts @@ -1,11 +1,11 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { BuilderContext } from '@angular-devkit/architect'; import { EmittedFiles } from '@angular-devkit/build-webpack'; -import { I18nOptions } from './i18n-options'; -export declare function i18nInlineEmittedFiles(context: BuilderContext, emittedFiles: EmittedFiles[], i18n: I18nOptions, baseOutputPath: string, outputPaths: string[], scriptsEntryPointName: string[], emittedPath: string, es5: boolean, missingTranslation: 'error' | 'warning' | 'ignore' | undefined): Promise; +import { I18nOptions } from './i18n-webpack'; +export declare function i18nInlineEmittedFiles(context: BuilderContext, emittedFiles: EmittedFiles[], i18n: I18nOptions, baseOutputPath: string, outputPaths: string[], scriptsEntryPointName: string[], emittedPath: string, missingTranslation: 'error' | 'warning' | 'ignore' | undefined): Promise; diff --git a/src/utils/i18n-inlining.js b/src/utils/i18n-inlining.js index c73b7752e..3914965d0 100644 --- a/src/utils/i18n-inlining.js +++ b/src/utils/i18n-inlining.js @@ -1,11 +1,53 @@ "use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); Object.defineProperty(exports, "__esModule", { value: true }); -exports.i18nInlineEmittedFiles = void 0; -const fs = require("fs"); -const path = require("path"); +exports.i18nInlineEmittedFiles = i18nInlineEmittedFiles; +const fs = __importStar(require("node:fs")); +const path = __importStar(require("node:path")); const action_executor_1 = require("./action-executor"); const copy_assets_1 = require("./copy-assets"); -function emittedFilesToInlineOptions(emittedFiles, scriptsEntryPointName, emittedPath, outputPath, es5, missingTranslation) { +const error_1 = require("./error"); +const spinner_1 = require("./spinner"); +function emittedFilesToInlineOptions(emittedFiles, scriptsEntryPointName, emittedPath, outputPath, missingTranslation, context) { const options = []; const originalFiles = []; for (const emittedFile of emittedFiles) { @@ -18,10 +60,9 @@ function emittedFilesToInlineOptions(emittedFiles, scriptsEntryPointName, emitte const action = { filename: emittedFile.file, code: fs.readFileSync(originalPath, 'utf8'), - es5, outputPath, missingTranslation, - setLocale: emittedFile.name === 'main' || emittedFile.name === 'vendor', + setLocale: emittedFile.name === 'main', }; originalFiles.push(originalPath); try { @@ -30,21 +71,27 @@ function emittedFilesToInlineOptions(emittedFiles, scriptsEntryPointName, emitte originalFiles.push(originalMapPath); } catch (err) { + (0, error_1.assertIsError)(err); if (err.code !== 'ENOENT') { throw err; } } + context.logger.debug(`i18n file queued for processing: ${action.filename}`); options.push(action); } return { options, originalFiles }; } -async function i18nInlineEmittedFiles(context, emittedFiles, i18n, baseOutputPath, outputPaths, scriptsEntryPointName, emittedPath, es5, missingTranslation) { +async function i18nInlineEmittedFiles(context, emittedFiles, i18n, baseOutputPath, outputPaths, scriptsEntryPointName, emittedPath, missingTranslation) { const executor = new action_executor_1.BundleActionExecutor({ i18n }); let hasErrors = false; + const spinner = new spinner_1.Spinner(); + spinner.start('Generating localized bundles...'); try { - const { options, originalFiles: processedFiles } = emittedFilesToInlineOptions(emittedFiles, scriptsEntryPointName, emittedPath, baseOutputPath, es5, missingTranslation); + const { options, originalFiles: processedFiles } = emittedFilesToInlineOptions(emittedFiles, scriptsEntryPointName, emittedPath, baseOutputPath, missingTranslation, context); for await (const result of executor.inlineAll(options)) { + context.logger.debug(`i18n file processed: ${result.file}`); for (const diagnostic of result.diagnostics) { + spinner.stop(); if (diagnostic.type === 'error') { hasErrors = true; context.logger.error(diagnostic.message); @@ -52,31 +99,32 @@ async function i18nInlineEmittedFiles(context, emittedFiles, i18n, baseOutputPat else { context.logger.warn(diagnostic.message); } + spinner.start(); } } // Copy any non-processed files into the output locations - await copy_assets_1.copyAssets([ + await (0, copy_assets_1.copyAssets)([ { glob: '**/*', input: emittedPath, output: '', - ignore: [...processedFiles].map(f => path.relative(emittedPath, f)), + ignore: [...processedFiles].map((f) => path.relative(emittedPath, f)), }, ], outputPaths, ''); } catch (err) { - context.logger.error('Localized bundle generation failed: ' + err.message); + (0, error_1.assertIsError)(err); + spinner.fail('Localized bundle generation failed: ' + err.message); return false; } finally { executor.stop(); } if (hasErrors) { - context.logger.error('Localized bundle generation failed.'); + spinner.fail('Localized bundle generation failed.'); } else { - context.logger.info('Localized bundle generation complete.'); + spinner.succeed('Localized bundle generation complete.'); } return !hasErrors; } -exports.i18nInlineEmittedFiles = i18nInlineEmittedFiles; diff --git a/src/utils/i18n-options.d.ts b/src/utils/i18n-options.d.ts deleted file mode 100644 index 8d923846f..000000000 --- a/src/utils/i18n-options.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { BuilderContext } from '@angular-devkit/architect'; -import { json } from '@angular-devkit/core'; -import { Schema as BrowserBuilderSchema } from '../browser/schema'; -import { Schema as ServerBuilderSchema } from '../server/schema'; -export interface I18nOptions { - inlineLocales: Set; - sourceLocale: string; - locales: Record; - flatOutput?: boolean; - readonly shouldInline: boolean; - veCompatLocale?: string; -} -export declare function createI18nOptions(metadata: json.JsonObject, inline?: boolean | string[]): I18nOptions; -export declare function configureI18nBuild(context: BuilderContext, options: T): Promise<{ - buildOptions: T; - i18n: I18nOptions; -}>; diff --git a/src/utils/i18n-options.js b/src/utils/i18n-options.js deleted file mode 100644 index 5c522d665..000000000 --- a/src/utils/i18n-options.js +++ /dev/null @@ -1,257 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.configureI18nBuild = exports.createI18nOptions = void 0; -const core_1 = require("@angular-devkit/core"); -const fs = require("fs"); -const os = require("os"); -const path = require("path"); -const rimraf = require("rimraf"); -const read_tsconfig_1 = require("../angular-cli-files/utilities/read-tsconfig"); -const load_translations_1 = require("./load-translations"); -function createI18nOptions(metadata, inline) { - if (metadata.i18n !== undefined && !core_1.json.isJsonObject(metadata.i18n)) { - throw new Error('Project i18n field is malformed. Expected an object.'); - } - metadata = metadata.i18n || {}; - const i18n = { - inlineLocales: new Set(), - // en-US is the default locale added to Angular applications (https://angular.io/guide/i18n#i18n-pipes) - sourceLocale: 'en-US', - locales: {}, - get shouldInline() { - return this.inlineLocales.size > 0; - }, - }; - let rawSourceLocale; - let rawSourceLocaleBaseHref; - if (core_1.json.isJsonObject(metadata.sourceLocale)) { - rawSourceLocale = metadata.sourceLocale.code; - if (metadata.sourceLocale.baseHref !== undefined && typeof metadata.sourceLocale.baseHref !== 'string') { - throw new Error('Project i18n sourceLocale baseHref field is malformed. Expected a string.'); - } - rawSourceLocaleBaseHref = metadata.sourceLocale.baseHref; - } - else { - rawSourceLocale = metadata.sourceLocale; - } - if (rawSourceLocale !== undefined) { - if (typeof rawSourceLocale !== 'string') { - throw new Error('Project i18n sourceLocale field is malformed. Expected a string.'); - } - i18n.sourceLocale = rawSourceLocale; - } - i18n.locales[i18n.sourceLocale] = { - file: '', - baseHref: rawSourceLocaleBaseHref, - }; - if (metadata.locales !== undefined && !core_1.json.isJsonObject(metadata.locales)) { - throw new Error('Project i18n locales field is malformed. Expected an object.'); - } - else if (metadata.locales) { - for (const [locale, options] of Object.entries(metadata.locales)) { - let translationFile; - let baseHref; - if (core_1.json.isJsonObject(options)) { - if (typeof options.translation !== 'string') { - throw new Error(`Project i18n locales translation field value for '${locale}' is malformed. Expected a string.`); - } - translationFile = options.translation; - if (typeof options.baseHref === 'string') { - baseHref = options.baseHref; - } - } - else if (typeof options !== 'string') { - throw new Error(`Project i18n locales field value for '${locale}' is malformed. Expected a string or object.`); - } - else { - translationFile = options; - } - if (locale === i18n.sourceLocale) { - throw new Error(`An i18n locale ('${locale}') cannot both be a source locale and provide a translation.`); - } - i18n.locales[locale] = { - file: translationFile, - baseHref, - }; - } - } - if (inline === true) { - i18n.inlineLocales.add(i18n.sourceLocale); - Object.keys(i18n.locales).forEach(locale => i18n.inlineLocales.add(locale)); - } - else if (inline) { - for (const locale of inline) { - if (!i18n.locales[locale] && i18n.sourceLocale !== locale) { - throw new Error(`Requested locale '${locale}' is not defined for the project.`); - } - i18n.inlineLocales.add(locale); - } - } - return i18n; -} -exports.createI18nOptions = createI18nOptions; -async function configureI18nBuild(context, options) { - if (!context.target) { - throw new Error('The builder requires a target.'); - } - const buildOptions = { ...options }; - const tsConfig = read_tsconfig_1.readTsconfig(buildOptions.tsConfig, context.workspaceRoot); - const usingIvy = tsConfig.options.enableIvy !== false; - const metadata = await context.getProjectMetadata(context.target); - const i18n = createI18nOptions(metadata, buildOptions.localize); - // Until 11.0, support deprecated i18n options when not using new localize option - // i18nFormat is automatically calculated - if (buildOptions.localize === undefined && usingIvy) { - mergeDeprecatedI18nOptions(i18n, buildOptions.i18nLocale, buildOptions.i18nFile); - } - else if (buildOptions.localize !== undefined && !usingIvy) { - if (buildOptions.localize === true || - (Array.isArray(buildOptions.localize) && buildOptions.localize.length > 1)) { - throw new Error(`Localization with multiple locales in one build is not supported with View Engine.`); - } - for (const deprecatedOption of ['i18nLocale', 'i18nFormat', 'i18nFile']) { - // tslint:disable-next-line: no-any - if (typeof buildOptions[deprecatedOption] !== 'undefined') { - context.logger.warn(`Option 'localize' and deprecated '${deprecatedOption}' found. Using 'localize'.`); - } - } - if (buildOptions.localize === false || - (Array.isArray(buildOptions.localize) && buildOptions.localize.length === 0)) { - buildOptions.i18nFile = undefined; - buildOptions.i18nLocale = undefined; - buildOptions.i18nFormat = undefined; - } - } - // Clear deprecated options when using Ivy to prevent unintended behavior - if (usingIvy) { - buildOptions.i18nFile = undefined; - buildOptions.i18nFormat = undefined; - buildOptions.i18nLocale = undefined; - } - if (i18n.inlineLocales.size > 0) { - const projectRoot = path.join(context.workspaceRoot, metadata.root || ''); - const localeDataBasePath = findLocaleDataBasePath(projectRoot); - if (!localeDataBasePath) { - throw new Error(`Unable to find locale data within '@angular/common'. Please ensure '@angular/common' is installed.`); - } - // Load locales - const loader = await load_translations_1.createTranslationLoader(); - const usedFormats = new Set(); - for (const [locale, desc] of Object.entries(i18n.locales)) { - if (!i18n.inlineLocales.has(locale)) { - continue; - } - let localeDataPath = findLocaleDataPath(locale, localeDataBasePath); - if (!localeDataPath) { - const [first] = locale.split('-'); - if (first) { - localeDataPath = findLocaleDataPath(first.toLowerCase(), localeDataBasePath); - if (localeDataPath) { - context.logger.warn(`Locale data for '${locale}' cannot be found. Using locale data for '${first}'.`); - } - } - } - if (!localeDataPath) { - context.logger.warn(`Locale data for '${locale}' cannot be found. No locale data will be included for this locale.`); - } - else { - desc.dataPath = localeDataPath; - } - if (!desc.file) { - continue; - } - const result = loader(path.join(context.workspaceRoot, desc.file)); - for (const diagnostics of result.diagnostics.messages) { - if (diagnostics.type === 'error') { - throw new Error(`Error parsing translation file '${desc.file}': ${diagnostics.message}`); - } - else { - context.logger.warn(`WARNING [${desc.file}]: ${diagnostics.message}`); - } - } - usedFormats.add(result.format); - if (usedFormats.size > 1 && tsConfig.options.enableI18nLegacyMessageIdFormat !== false) { - // This limitation is only for legacy message id support (defaults to true as of 9.0) - throw new Error('Localization currently only supports using one type of translation file format for the entire application.'); - } - desc.format = result.format; - desc.translation = result.translation; - desc.integrity = result.integrity; - } - // Legacy message id's require the format of the translations - if (usedFormats.size > 0) { - buildOptions.i18nFormat = [...usedFormats][0]; - } - // Provide support for using the Ivy i18n options with VE - if (!usingIvy) { - i18n.veCompatLocale = buildOptions.i18nLocale = [...i18n.inlineLocales][0]; - if (buildOptions.i18nLocale !== i18n.sourceLocale) { - buildOptions.i18nFile = i18n.locales[buildOptions.i18nLocale].file; - } - // Clear inline locales to prevent any new i18n related processing - i18n.inlineLocales.clear(); - // Update the output path to include the locale to mimic Ivy localize behavior - buildOptions.outputPath = path.join(buildOptions.outputPath, buildOptions.i18nLocale); - } - } - // If inlining store the output in a temporary location to facilitate post-processing - if (i18n.shouldInline) { - const tempPath = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'angular-cli-i18n-')); - buildOptions.outputPath = tempPath; - // Remove temporary directory used for i18n processing - process.on('exit', () => { - try { - rimraf.sync(tempPath); - } - catch (_a) { } - }); - } - return { buildOptions, i18n }; -} -exports.configureI18nBuild = configureI18nBuild; -function mergeDeprecatedI18nOptions(i18n, i18nLocale, i18nFile) { - if (i18nFile !== undefined && i18nLocale === undefined) { - throw new Error(`Option 'i18nFile' cannot be used without the 'i18nLocale' option.`); - } - if (i18nLocale !== undefined) { - i18n.inlineLocales.clear(); - i18n.inlineLocales.add(i18nLocale); - if (i18nFile !== undefined) { - i18n.locales[i18nLocale] = { file: i18nFile, baseHref: '' }; - } - else { - // If no file, treat the locale as the source locale - // This mimics deprecated behavior - i18n.sourceLocale = i18nLocale; - i18n.locales[i18nLocale] = { file: '', baseHref: '' }; - } - i18n.flatOutput = true; - } - return i18n; -} -function findLocaleDataBasePath(projectRoot) { - try { - const commonPath = path.dirname(require.resolve('@angular/common/package.json', { paths: [projectRoot] })); - const localesPath = path.join(commonPath, 'locales/global'); - if (!fs.existsSync(localesPath)) { - return null; - } - return localesPath; - } - catch (_a) { - return null; - } -} -function findLocaleDataPath(locale, basePath) { - // Remove private use subtags - const scrubbedLocale = locale.replace(/-x(-[a-zA-Z0-9]{1,8})+$/, ''); - const localeDataPath = path.join(basePath, scrubbedLocale + '.js'); - if (!fs.existsSync(localeDataPath)) { - if (scrubbedLocale === 'en-US') { - // fallback to known existing en-US locale data as of 9.0 - return findLocaleDataPath('en-US-POSIX', basePath); - } - return null; - } - return localeDataPath; -} diff --git a/src/utils/i18n-webpack.d.ts b/src/utils/i18n-webpack.d.ts new file mode 100644 index 000000000..0462d5ca6 --- /dev/null +++ b/src/utils/i18n-webpack.d.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { type I18nOptions, loadTranslations } from '@angular/build/private'; +import { BuilderContext } from '@angular-devkit/architect'; +import { Schema as BrowserBuilderSchema } from '../builders/browser/schema'; +import { Schema as ServerBuilderSchema } from '../builders/server/schema'; +export { I18nOptions, loadTranslations }; +export declare function configureI18nBuild(context: BuilderContext, options: T): Promise<{ + buildOptions: T; + i18n: I18nOptions; +}>; diff --git a/src/utils/i18n-webpack.js b/src/utils/i18n-webpack.js new file mode 100644 index 000000000..2902e2f93 --- /dev/null +++ b/src/utils/i18n-webpack.js @@ -0,0 +1,107 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.loadTranslations = void 0; +exports.configureI18nBuild = configureI18nBuild; +const private_1 = require("@angular/build/private"); +Object.defineProperty(exports, "loadTranslations", { enumerable: true, get: function () { return private_1.loadTranslations; } }); +const node_fs_1 = __importDefault(require("node:fs")); +const node_module_1 = require("node:module"); +const node_os_1 = __importDefault(require("node:os")); +const node_path_1 = __importDefault(require("node:path")); +const read_tsconfig_1 = require("../utils/read-tsconfig"); +/** + * The base module location used to search for locale specific data. + */ +const LOCALE_DATA_BASE_MODULE = '@angular/common/locales/global'; +async function configureI18nBuild(context, options) { + if (!context.target) { + throw new Error('The builder requires a target.'); + } + const buildOptions = { ...options }; + const tsConfig = await (0, read_tsconfig_1.readTsconfig)(buildOptions.tsConfig, context.workspaceRoot); + const metadata = await context.getProjectMetadata(context.target); + const i18n = (0, private_1.createI18nOptions)(metadata, buildOptions.localize, context.logger); + // No additional processing needed if no inlining requested and no source locale defined. + if (!i18n.shouldInline && !i18n.hasDefinedSourceLocale) { + return { buildOptions, i18n }; + } + const projectRoot = node_path_1.default.join(context.workspaceRoot, metadata.root || ''); + // The trailing slash is required to signal that the path is a directory and not a file. + const projectRequire = (0, node_module_1.createRequire)(projectRoot + '/'); + const localeResolver = (locale) => projectRequire.resolve(node_path_1.default.join(LOCALE_DATA_BASE_MODULE, locale)); + // Load locale data and translations (if present) + let loader; + const usedFormats = new Set(); + for (const [locale, desc] of Object.entries(i18n.locales)) { + if (!i18n.inlineLocales.has(locale) && locale !== i18n.sourceLocale) { + continue; + } + let localeDataPath = findLocaleDataPath(locale, localeResolver); + if (!localeDataPath) { + const [first] = locale.split('-'); + if (first) { + localeDataPath = findLocaleDataPath(first.toLowerCase(), localeResolver); + if (localeDataPath) { + context.logger.warn(`Locale data for '${locale}' cannot be found. Using locale data for '${first}'.`); + } + } + } + if (!localeDataPath) { + context.logger.warn(`Locale data for '${locale}' cannot be found. No locale data will be included for this locale.`); + } + else { + desc.dataPath = localeDataPath; + } + if (!desc.files.length) { + continue; + } + loader ??= await (0, private_1.createTranslationLoader)(); + (0, private_1.loadTranslations)(locale, desc, context.workspaceRoot, loader, { + warn(message) { + context.logger.warn(message); + }, + error(message) { + throw new Error(message); + }, + }, usedFormats, buildOptions.i18nDuplicateTranslation); + if (usedFormats.size > 1 && tsConfig.options.enableI18nLegacyMessageIdFormat !== false) { + // This limitation is only for legacy message id support (defaults to true as of 9.0) + throw new Error('Localization currently only supports using one type of translation file format for the entire application.'); + } + } + // If inlining store the output in a temporary location to facilitate post-processing + if (i18n.shouldInline) { + // TODO: we should likely save these in the .angular directory in the next major version. + // We'd need to do a migration to add the temp directory to gitignore. + const tempPath = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_fs_1.default.realpathSync(node_os_1.default.tmpdir()), 'angular-cli-i18n-')); + buildOptions.outputPath = tempPath; + process.on('exit', () => { + try { + node_fs_1.default.rmSync(tempPath, { force: true, recursive: true, maxRetries: 3 }); + } + catch { } + }); + } + return { buildOptions, i18n }; +} +function findLocaleDataPath(locale, resolver) { + // Remove private use subtags + const scrubbedLocale = locale.replace(/-x(-[a-zA-Z0-9]{1,8})+$/, ''); + try { + return resolver(scrubbedLocale); + } + catch { + // fallback to known existing en-US locale data as of 14.0 + return scrubbedLocale === 'en-US' ? findLocaleDataPath('en', resolver) : null; + } +} diff --git a/src/utils/index.d.ts b/src/utils/index.d.ts index 32b5b9dd7..a163060bc 100644 --- a/src/utils/index.d.ts +++ b/src/utils/index.d.ts @@ -1,13 +1,12 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -export * from './build-browser-features'; export * from './default-progress'; -export * from './delete-output-dir'; +export { deleteOutputDir, loadProxyConfiguration } from '@angular/build/private'; export * from './run-module-as-observable-fork'; export * from './normalize-file-replacements'; export * from './normalize-asset-patterns'; @@ -15,4 +14,3 @@ export * from './normalize-source-maps'; export * from './normalize-optimization'; export * from './normalize-builder-schema'; export * from './url'; -export * from './workers'; diff --git a/src/utils/index.js b/src/utils/index.js index d83d36876..ea8a9dc54 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,25 +1,31 @@ "use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("./build-browser-features"), exports); +exports.loadProxyConfiguration = exports.deleteOutputDir = void 0; __exportStar(require("./default-progress"), exports); -__exportStar(require("./delete-output-dir"), exports); +var private_1 = require("@angular/build/private"); +Object.defineProperty(exports, "deleteOutputDir", { enumerable: true, get: function () { return private_1.deleteOutputDir; } }); +Object.defineProperty(exports, "loadProxyConfiguration", { enumerable: true, get: function () { return private_1.loadProxyConfiguration; } }); __exportStar(require("./run-module-as-observable-fork"), exports); __exportStar(require("./normalize-file-replacements"), exports); __exportStar(require("./normalize-asset-patterns"), exports); @@ -27,4 +33,3 @@ __exportStar(require("./normalize-source-maps"), exports); __exportStar(require("./normalize-optimization"), exports); __exportStar(require("./normalize-builder-schema"), exports); __exportStar(require("./url"), exports); -__exportStar(require("./workers"), exports); diff --git a/src/utils/load-esm.d.ts b/src/utils/load-esm.d.ts new file mode 100644 index 000000000..45ae8f437 --- /dev/null +++ b/src/utils/load-esm.d.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +/** + * This uses a dynamic import to load a module which may be ESM. + * CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript + * will currently, unconditionally downlevel dynamic import into a require call. + * require calls cannot load ESM code and will result in a runtime error. To workaround + * this, a Function constructor is used to prevent TypeScript from changing the dynamic import. + * Once TypeScript provides support for keeping the dynamic import this workaround can + * be dropped. + * + * @param modulePath The path of the module to load. + * @returns A Promise that resolves to the dynamically imported module. + */ +export declare function loadEsmModule(modulePath: string | URL): Promise; diff --git a/src/utils/load-esm.js b/src/utils/load-esm.js new file mode 100644 index 000000000..b703a110b --- /dev/null +++ b/src/utils/load-esm.js @@ -0,0 +1,30 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.loadEsmModule = loadEsmModule; +/** + * Lazily compiled dynamic import loader function. + */ +let load; +/** + * This uses a dynamic import to load a module which may be ESM. + * CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript + * will currently, unconditionally downlevel dynamic import into a require call. + * require calls cannot load ESM code and will result in a runtime error. To workaround + * this, a Function constructor is used to prevent TypeScript from changing the dynamic import. + * Once TypeScript provides support for keeping the dynamic import this workaround can + * be dropped. + * + * @param modulePath The path of the module to load. + * @returns A Promise that resolves to the dynamically imported module. + */ +function loadEsmModule(modulePath) { + load ??= new Function('modulePath', `return import(modulePath);`); + return load(modulePath); +} diff --git a/src/utils/load-translations.d.ts b/src/utils/load-translations.d.ts deleted file mode 100644 index d3916b116..000000000 --- a/src/utils/load-translations.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export declare type TranslationLoader = (path: string) => { - translation: unknown; - format: string; - diagnostics: import('@angular/localize/src/tools/src/diagnostics').Diagnostics; - integrity: string; -}; -export declare function createTranslationLoader(): Promise; diff --git a/src/utils/load-translations.js b/src/utils/load-translations.js deleted file mode 100644 index 063fb7157..000000000 --- a/src/utils/load-translations.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createTranslationLoader = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const crypto_1 = require("crypto"); -const fs = require("fs"); -async function createTranslationLoader() { - const { parsers, diagnostics } = await importParsers(); - return (path) => { - const content = fs.readFileSync(path, 'utf8'); - for (const [format, parser] of Object.entries(parsers)) { - if (parser.canParse(path, content)) { - const result = parser.parse(path, content); - const integrity = 'sha256-' + crypto_1.createHash('sha256').update(content).digest('base64'); - return { format, translation: result.translations, diagnostics, integrity }; - } - } - throw new Error('Unsupported translation file format.'); - }; -} -exports.createTranslationLoader = createTranslationLoader; -async function importParsers() { - try { - // tslint:disable-next-line: no-implicit-dependencies - const localizeDiag = await Promise.resolve().then(() => require('@angular/localize/src/tools/src/diagnostics')); - const diagnostics = new localizeDiag.Diagnostics(); - const parsers = { - json: new (await Promise.resolve().then(() => require( - // tslint:disable-next-line:trailing-comma no-implicit-dependencies - '@angular/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser'))).SimpleJsonTranslationParser(), - xlf: new (await Promise.resolve().then(() => require( - // tslint:disable-next-line:trailing-comma no-implicit-dependencies - '@angular/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser'))).Xliff1TranslationParser(), - xlf2: new (await Promise.resolve().then(() => require( - // tslint:disable-next-line:trailing-comma no-implicit-dependencies - '@angular/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser'))).Xliff2TranslationParser(), - // The name ('xmb') needs to match the AOT compiler option - xmb: new (await Promise.resolve().then(() => require( - // tslint:disable-next-line:trailing-comma no-implicit-dependencies - '@angular/localize/src/tools/src/translate/translation_files/translation_parsers/xtb_translation_parser'))).XtbTranslationParser(), - }; - return { parsers, diagnostics }; - } - catch (_a) { - throw new Error(`Unable to load translation file parsers. Please ensure '@angular/localize' is installed.`); - } -} diff --git a/src/utils/normalize-asset-patterns.d.ts b/src/utils/normalize-asset-patterns.d.ts index 0c52e94ef..ac4d42c89 100644 --- a/src/utils/normalize-asset-patterns.d.ts +++ b/src/utils/normalize-asset-patterns.d.ts @@ -1,13 +1,14 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { BaseException, Path, virtualFs } from '@angular-devkit/core'; -import { AssetPattern, AssetPatternClass } from '../browser/schema'; -export declare class MissingAssetSourceRootException extends BaseException { - constructor(path: String); +import { AssetPattern, AssetPatternClass } from '../builders/browser/schema'; +export declare class MissingAssetSourceRootException extends Error { + constructor(path: string); } -export declare function normalizeAssetPatterns(assetPatterns: AssetPattern[], host: virtualFs.SyncDelegateHost, root: Path, projectRoot: Path, maybeSourceRoot: Path | undefined): AssetPatternClass[]; +export declare function normalizeAssetPatterns(assetPatterns: AssetPattern[], workspaceRoot: string, projectRoot: string, projectSourceRoot: string | undefined): (AssetPatternClass & { + output: string; +})[]; diff --git a/src/utils/normalize-asset-patterns.js b/src/utils/normalize-asset-patterns.js index 3d118530a..c7716c443 100644 --- a/src/utils/normalize-asset-patterns.js +++ b/src/utils/normalize-asset-patterns.js @@ -1,43 +1,81 @@ "use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.normalizeAssetPatterns = exports.MissingAssetSourceRootException = void 0; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -const core_1 = require("@angular-devkit/core"); -class MissingAssetSourceRootException extends core_1.BaseException { +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MissingAssetSourceRootException = void 0; +exports.normalizeAssetPatterns = normalizeAssetPatterns; +const node_assert_1 = __importDefault(require("node:assert")); +const node_fs_1 = require("node:fs"); +const path = __importStar(require("node:path")); +class MissingAssetSourceRootException extends Error { constructor(path) { super(`The ${path} asset path must start with the project source root.`); } } exports.MissingAssetSourceRootException = MissingAssetSourceRootException; -function normalizeAssetPatterns(assetPatterns, host, root, projectRoot, maybeSourceRoot) { - // When sourceRoot is not available, we default to ${projectRoot}/src. - const sourceRoot = maybeSourceRoot || core_1.join(projectRoot, 'src'); - const resolvedSourceRoot = core_1.resolve(root, sourceRoot); +function normalizeAssetPatterns(assetPatterns, workspaceRoot, projectRoot, projectSourceRoot) { if (assetPatterns.length === 0) { return []; } - return assetPatterns - .map(assetPattern => { + // When sourceRoot is not available, we default to ${projectRoot}/src. + const sourceRoot = projectSourceRoot || path.join(projectRoot, 'src'); + const resolvedSourceRoot = path.resolve(workspaceRoot, sourceRoot); + return assetPatterns.map((assetPattern) => { // Normalize string asset patterns to objects. if (typeof assetPattern === 'string') { - const assetPath = core_1.normalize(assetPattern); - const resolvedAssetPath = core_1.resolve(root, assetPath); + const assetPath = path.normalize(assetPattern); + const resolvedAssetPath = path.resolve(workspaceRoot, assetPath); // Check if the string asset is within sourceRoot. if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) { throw new MissingAssetSourceRootException(assetPattern); } - let glob, input, output; + let glob, input; let isDirectory = false; try { - isDirectory = host.isDirectory(resolvedAssetPath); + isDirectory = (0, node_fs_1.statSync)(resolvedAssetPath).isDirectory(); } - catch (_a) { + catch { isDirectory = true; } if (isDirectory) { @@ -48,19 +86,21 @@ function normalizeAssetPatterns(assetPatterns, host, root, projectRoot, maybeSou } else { // Files are their own glob. - glob = core_1.basename(assetPath); + glob = path.basename(assetPath); // Input directory is their original dirname. - input = core_1.dirname(assetPath); + input = path.dirname(assetPath); } // Output directory for both is the relative path from source root to input. - output = core_1.relative(resolvedSourceRoot, core_1.resolve(root, input)); - // Return the asset pattern in object format. - return { glob, input, output }; + const output = path.relative(resolvedSourceRoot, path.resolve(workspaceRoot, input)); + assetPattern = { glob, input, output }; } else { - // It's already an AssetPatternObject, no need to convert. - return assetPattern; + assetPattern.output = path.join('.', assetPattern.output ?? ''); } + (0, node_assert_1.default)(assetPattern.output !== undefined); + if (assetPattern.output.startsWith('..')) { + throw new Error('An asset cannot be written to a location outside of the output path.'); + } + return assetPattern; }); } -exports.normalizeAssetPatterns = normalizeAssetPatterns; diff --git a/src/utils/normalize-builder-schema.d.ts b/src/utils/normalize-builder-schema.d.ts index 9e9cb0747..d6b53b0c0 100644 --- a/src/utils/normalize-builder-schema.d.ts +++ b/src/utils/normalize-builder-schema.d.ts @@ -1,21 +1,23 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { Path, virtualFs } from '@angular-devkit/core'; -import { BuildOptions } from '../angular-cli-files/models/build-options'; -import { AssetPatternClass, OptimizationClass, Schema as BrowserBuilderSchema, SourceMapClass } from '../browser/schema'; +import { json, logging } from '@angular-devkit/core'; +import { AssetPatternClass, Schema as BrowserBuilderSchema, SourceMapClass } from '../builders/browser/schema'; +import { BuildOptions } from './build-options'; import { NormalizedFileReplacement } from './normalize-file-replacements'; +import { NormalizedOptimizationOptions } from './normalize-optimization'; /** * A normalized browser builder schema. */ -export declare type NormalizedBrowserBuilderSchema = BrowserBuilderSchema & BuildOptions & { +export type NormalizedBrowserBuilderSchema = BrowserBuilderSchema & BuildOptions & { sourceMap: SourceMapClass; assets: AssetPatternClass[]; fileReplacements: NormalizedFileReplacement[]; - optimization: OptimizationClass; + optimization: NormalizedOptimizationOptions; + polyfills: string[]; }; -export declare function normalizeBrowserSchema(host: virtualFs.Host<{}>, root: Path, projectRoot: Path, sourceRoot: Path | undefined, options: BrowserBuilderSchema): NormalizedBrowserBuilderSchema; +export declare function normalizeBrowserSchema(workspaceRoot: string, projectRoot: string, projectSourceRoot: string | undefined, options: BrowserBuilderSchema, metadata: json.JsonObject, logger: logging.LoggerApi): NormalizedBrowserBuilderSchema; diff --git a/src/utils/normalize-builder-schema.js b/src/utils/normalize-builder-schema.js index 87546c616..36f216111 100644 --- a/src/utils/normalize-builder-schema.js +++ b/src/utils/normalize-builder-schema.js @@ -1,43 +1,43 @@ "use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.normalizeBrowserSchema = void 0; -const core_1 = require("@angular-devkit/core"); +exports.normalizeBrowserSchema = normalizeBrowserSchema; +const private_1 = require("@angular/build/private"); const normalize_asset_patterns_1 = require("./normalize-asset-patterns"); +const normalize_cache_1 = require("./normalize-cache"); const normalize_file_replacements_1 = require("./normalize-file-replacements"); const normalize_optimization_1 = require("./normalize-optimization"); +const normalize_polyfills_1 = require("./normalize-polyfills"); const normalize_source_maps_1 = require("./normalize-source-maps"); -function normalizeBrowserSchema(host, root, projectRoot, sourceRoot, options) { - const syncHost = new core_1.virtualFs.SyncDelegateHost(host); - const normalizedSourceMapOptions = normalize_source_maps_1.normalizeSourceMaps(options.sourceMap || false); +function normalizeBrowserSchema(workspaceRoot, projectRoot, projectSourceRoot, options, metadata, logger) { return { ...options, - assets: normalize_asset_patterns_1.normalizeAssetPatterns(options.assets || [], syncHost, root, projectRoot, sourceRoot), - fileReplacements: normalize_file_replacements_1.normalizeFileReplacements(options.fileReplacements || [], syncHost, root), - optimization: normalize_optimization_1.normalizeOptimization(options.optimization), - sourceMap: normalizedSourceMapOptions, - preserveSymlinks: options.preserveSymlinks === undefined ? process.execArgv.includes('--preserve-symlinks') : options.preserveSymlinks, + cache: (0, normalize_cache_1.normalizeCacheOptions)(metadata, workspaceRoot), + assets: (0, normalize_asset_patterns_1.normalizeAssetPatterns)(options.assets || [], workspaceRoot, projectRoot, projectSourceRoot), + fileReplacements: (0, normalize_file_replacements_1.normalizeFileReplacements)(options.fileReplacements || [], workspaceRoot), + optimization: (0, normalize_optimization_1.normalizeOptimization)(options.optimization), + sourceMap: (0, normalize_source_maps_1.normalizeSourceMaps)(options.sourceMap || false), + polyfills: (0, normalize_polyfills_1.normalizePolyfills)(options.polyfills, workspaceRoot), + preserveSymlinks: options.preserveSymlinks === undefined + ? process.execArgv.includes('--preserve-symlinks') + : options.preserveSymlinks, statsJson: options.statsJson || false, - forkTypeChecker: options.forkTypeChecker || false, budgets: options.budgets || [], scripts: options.scripts || [], styles: options.styles || [], stylePreprocessorOptions: { - includePaths: options.stylePreprocessorOptions - && options.stylePreprocessorOptions.includePaths - || [], + includePaths: (options.stylePreprocessorOptions && options.stylePreprocessorOptions.includePaths) || [], }, - lazyModules: options.lazyModules || [], // Using just `--poll` will result in a value of 0 which is very likely not the intention // A value of 0 is falsy and will disable polling rather then enable // 500 ms is a sensible default in this case poll: options.poll === 0 ? 500 : options.poll, + supportedBrowsers: (0, private_1.getSupportedBrowsers)(projectRoot, logger), }; } -exports.normalizeBrowserSchema = normalizeBrowserSchema; diff --git a/src/utils/normalize-cache.d.ts b/src/utils/normalize-cache.d.ts new file mode 100644 index 000000000..6d761941b --- /dev/null +++ b/src/utils/normalize-cache.d.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export interface NormalizedCachedOptions { + /** Whether disk cache is enabled. */ + enabled: boolean; + /** Disk cache path. Example: `/.angular/cache/v12.0.0`. */ + path: string; + /** Disk cache base path. Example: `/.angular/cache`. */ + basePath: string; +} +export declare function normalizeCacheOptions(projectMetadata: unknown, worspaceRoot: string): NormalizedCachedOptions; diff --git a/src/utils/normalize-cache.js b/src/utils/normalize-cache.js new file mode 100644 index 000000000..487c84a00 --- /dev/null +++ b/src/utils/normalize-cache.js @@ -0,0 +1,43 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeCacheOptions = normalizeCacheOptions; +const node_path_1 = require("node:path"); +/** Version placeholder is replaced during the build process with actual package version */ +const VERSION = '21.0.0-next.6+sha-2d41699'; +function hasCacheMetadata(value) { + return (!!value && + typeof value === 'object' && + 'cli' in value && + !!value['cli'] && + typeof value['cli'] === 'object' && + 'cache' in value['cli']); +} +function normalizeCacheOptions(projectMetadata, worspaceRoot) { + const cacheMetadata = hasCacheMetadata(projectMetadata) ? projectMetadata.cli.cache : {}; + const { enabled = true, environment = 'local', path = '.angular/cache' } = cacheMetadata; + const isCI = process.env['CI'] === '1' || process.env['CI']?.toLowerCase() === 'true'; + let cacheEnabled = enabled; + if (cacheEnabled) { + switch (environment) { + case 'ci': + cacheEnabled = isCI; + break; + case 'local': + cacheEnabled = !isCI; + break; + } + } + const cacheBasePath = (0, node_path_1.resolve)(worspaceRoot, path); + return { + enabled: cacheEnabled, + basePath: cacheBasePath, + path: (0, node_path_1.join)(cacheBasePath, VERSION), + }; +} diff --git a/src/utils/normalize-file-replacements.d.ts b/src/utils/normalize-file-replacements.d.ts index f4febf972..870d6561f 100644 --- a/src/utils/normalize-file-replacements.d.ts +++ b/src/utils/normalize-file-replacements.d.ts @@ -1,17 +1,16 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { BaseException, Path, virtualFs } from '@angular-devkit/core'; -import { FileReplacement } from '../browser/schema'; -export declare class MissingFileReplacementException extends BaseException { - constructor(path: String); +import { FileReplacement } from '../builders/browser/schema'; +export declare class MissingFileReplacementException extends Error { + constructor(path: string); } export interface NormalizedFileReplacement { - replace: Path; - with: Path; + replace: string; + with: string; } -export declare function normalizeFileReplacements(fileReplacements: FileReplacement[], host: virtualFs.SyncDelegateHost, root: Path): NormalizedFileReplacement[]; +export declare function normalizeFileReplacements(fileReplacements: FileReplacement[], workspaceRoot: string): NormalizedFileReplacement[]; diff --git a/src/utils/normalize-file-replacements.js b/src/utils/normalize-file-replacements.js index 4d4a8b1e2..fddd45652 100644 --- a/src/utils/normalize-file-replacements.js +++ b/src/utils/normalize-file-replacements.js @@ -1,57 +1,86 @@ "use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); Object.defineProperty(exports, "__esModule", { value: true }); -exports.normalizeFileReplacements = exports.MissingFileReplacementException = void 0; -const core_1 = require("@angular-devkit/core"); -class MissingFileReplacementException extends core_1.BaseException { +exports.MissingFileReplacementException = void 0; +exports.normalizeFileReplacements = normalizeFileReplacements; +const node_fs_1 = require("node:fs"); +const path = __importStar(require("node:path")); +class MissingFileReplacementException extends Error { constructor(path) { super(`The ${path} path in file replacements does not exist.`); } } exports.MissingFileReplacementException = MissingFileReplacementException; -function normalizeFileReplacements(fileReplacements, host, root) { +function normalizeFileReplacements(fileReplacements, workspaceRoot) { if (fileReplacements.length === 0) { return []; } - const normalizedReplacement = fileReplacements - .map(replacement => normalizeFileReplacement(replacement, root)); + const normalizedReplacement = fileReplacements.map((replacement) => normalizeFileReplacement(replacement, workspaceRoot)); for (const { replace, with: replacementWith } of normalizedReplacement) { - if (!host.exists(replacementWith)) { - throw new MissingFileReplacementException(core_1.getSystemPath(replacementWith)); + if (!(0, node_fs_1.existsSync)(replacementWith)) { + throw new MissingFileReplacementException(replacementWith); } - if (!host.exists(replace)) { - throw new MissingFileReplacementException(core_1.getSystemPath(replace)); + if (!(0, node_fs_1.existsSync)(replace)) { + throw new MissingFileReplacementException(replace); } } return normalizedReplacement; } -exports.normalizeFileReplacements = normalizeFileReplacements; function normalizeFileReplacement(fileReplacement, root) { let replacePath; let withPath; if (fileReplacement.src && fileReplacement.replaceWith) { - replacePath = core_1.normalize(fileReplacement.src); - withPath = core_1.normalize(fileReplacement.replaceWith); + replacePath = fileReplacement.src; + withPath = fileReplacement.replaceWith; } else if (fileReplacement.replace && fileReplacement.with) { - replacePath = core_1.normalize(fileReplacement.replace); - withPath = core_1.normalize(fileReplacement.with); + replacePath = fileReplacement.replace; + withPath = fileReplacement.with; } else { throw new Error(`Invalid file replacement: ${JSON.stringify(fileReplacement)}`); } - // TODO: For 7.x should this only happen if not absolute? - if (root) { - replacePath = core_1.join(root, replacePath); - } - if (root) { - withPath = core_1.join(root, withPath); - } - return { replace: replacePath, with: withPath }; + return { + replace: path.join(root, replacePath), + with: path.join(root, withPath), + }; } diff --git a/src/utils/normalize-optimization.d.ts b/src/utils/normalize-optimization.d.ts index ff8b0f355..a51f8bf8c 100644 --- a/src/utils/normalize-optimization.d.ts +++ b/src/utils/normalize-optimization.d.ts @@ -1,9 +1,13 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { OptimizationClass, OptimizationUnion } from '../browser/schema'; -export declare function normalizeOptimization(optimization?: OptimizationUnion): Required; +import { FontsClass, OptimizationClass, OptimizationUnion, StylesClass } from '../builders/browser/schema'; +export type NormalizedOptimizationOptions = Required> & { + fonts: FontsClass; + styles: StylesClass; +}; +export declare function normalizeOptimization(optimization?: OptimizationUnion): NormalizedOptimizationOptions; diff --git a/src/utils/normalize-optimization.js b/src/utils/normalize-optimization.js index 33091c2e3..143ac7363 100644 --- a/src/utils/normalize-optimization.js +++ b/src/utils/normalize-optimization.js @@ -1,17 +1,39 @@ "use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.normalizeOptimization = void 0; -function normalizeOptimization(optimization = false) { +exports.normalizeOptimization = normalizeOptimization; +function normalizeOptimization(optimization = true) { + if (typeof optimization === 'object') { + const styleOptimization = !!optimization.styles; + return { + scripts: !!optimization.scripts, + styles: typeof optimization.styles === 'object' + ? optimization.styles + : { + minify: styleOptimization, + inlineCritical: styleOptimization, + }, + fonts: typeof optimization.fonts === 'object' + ? optimization.fonts + : { + inline: !!optimization.fonts, + }, + }; + } return { - scripts: typeof optimization === 'object' ? !!optimization.scripts : optimization, - styles: typeof optimization === 'object' ? !!optimization.styles : optimization, + scripts: optimization, + styles: { + minify: optimization, + inlineCritical: optimization, + }, + fonts: { + inline: optimization, + }, }; } -exports.normalizeOptimization = normalizeOptimization; diff --git a/src/utils/normalize-polyfills.d.ts b/src/utils/normalize-polyfills.d.ts new file mode 100644 index 000000000..109491318 --- /dev/null +++ b/src/utils/normalize-polyfills.d.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export declare function normalizePolyfills(polyfills: string[] | string | undefined, root: string): string[]; diff --git a/src/utils/normalize-polyfills.js b/src/utils/normalize-polyfills.js new file mode 100644 index 000000000..01ecd836c --- /dev/null +++ b/src/utils/normalize-polyfills.js @@ -0,0 +1,23 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizePolyfills = normalizePolyfills; +const node_fs_1 = require("node:fs"); +const node_path_1 = require("node:path"); +function normalizePolyfills(polyfills, root) { + if (!polyfills) { + return []; + } + const polyfillsList = Array.isArray(polyfills) ? polyfills : [polyfills]; + return polyfillsList.map((p) => { + const resolvedPath = (0, node_path_1.resolve)(root, p); + // If file doesn't exist, let the bundle resolve it using node module resolution. + return (0, node_fs_1.existsSync)(resolvedPath) ? resolvedPath : p; + }); +} diff --git a/src/utils/normalize-source-maps.d.ts b/src/utils/normalize-source-maps.d.ts index d2777eeaa..9e2d3136b 100644 --- a/src/utils/normalize-source-maps.d.ts +++ b/src/utils/normalize-source-maps.d.ts @@ -1,9 +1,9 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { SourceMapClass, SourceMapUnion } from '../browser/schema'; +import { SourceMapClass, SourceMapUnion } from '../builders/browser/schema'; export declare function normalizeSourceMaps(sourceMap: SourceMapUnion): SourceMapClass; diff --git a/src/utils/normalize-source-maps.js b/src/utils/normalize-source-maps.js index 2a8d315fa..e580c88a6 100644 --- a/src/utils/normalize-source-maps.js +++ b/src/utils/normalize-source-maps.js @@ -1,18 +1,18 @@ "use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.normalizeSourceMaps = void 0; +exports.normalizeSourceMaps = normalizeSourceMaps; function normalizeSourceMaps(sourceMap) { const scripts = typeof sourceMap === 'object' ? sourceMap.scripts : sourceMap; const styles = typeof sourceMap === 'object' ? sourceMap.styles : sourceMap; - const hidden = typeof sourceMap === 'object' && sourceMap.hidden || false; - const vendor = typeof sourceMap === 'object' && sourceMap.vendor || false; + const hidden = (typeof sourceMap === 'object' && sourceMap.hidden) || false; + const vendor = (typeof sourceMap === 'object' && sourceMap.vendor) || false; return { vendor, hidden, @@ -20,4 +20,3 @@ function normalizeSourceMaps(sourceMap) { styles, }; } -exports.normalizeSourceMaps = normalizeSourceMaps; diff --git a/src/utils/output-paths.d.ts b/src/utils/output-paths.d.ts index 9d48e7c03..c9549c700 100644 --- a/src/utils/output-paths.d.ts +++ b/src/utils/output-paths.d.ts @@ -1,2 +1,9 @@ -import { I18nOptions } from './i18n-options'; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { I18nOptions } from './i18n-webpack'; export declare function ensureOutputPaths(baseOutputPath: string, i18n: I18nOptions): Map; diff --git a/src/utils/output-paths.js b/src/utils/output-paths.js index baf54f3a7..968140b0c 100644 --- a/src/utils/output-paths.js +++ b/src/utils/output-paths.js @@ -1,28 +1,26 @@ "use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ensureOutputPaths = void 0; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -const fs_1 = require("fs"); -const path_1 = require("path"); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ensureOutputPaths = ensureOutputPaths; +const node_fs_1 = require("node:fs"); +const node_path_1 = require("node:path"); function ensureOutputPaths(baseOutputPath, i18n) { const outputPaths = i18n.shouldInline - ? [...i18n.inlineLocales].map(l => [l, i18n.flatOutput ? baseOutputPath : path_1.join(baseOutputPath, l)]) - : [ - i18n.veCompatLocale - ? [i18n.veCompatLocale, path_1.join(baseOutputPath, i18n.veCompatLocale)] - : ['', baseOutputPath], - ]; + ? [...i18n.inlineLocales].map((l) => [ + l, + i18n.flatOutput ? baseOutputPath : (0, node_path_1.join)(baseOutputPath, i18n.locales[l].subPath), + ]) + : [['', baseOutputPath]]; for (const [, outputPath] of outputPaths) { - if (!fs_1.existsSync(outputPath)) { - fs_1.mkdirSync(outputPath, { recursive: true }); + if (!(0, node_fs_1.existsSync)(outputPath)) { + (0, node_fs_1.mkdirSync)(outputPath, { recursive: true }); } } return new Map(outputPaths); } -exports.ensureOutputPaths = ensureOutputPaths; diff --git a/src/utils/package-chunk-sort.d.ts b/src/utils/package-chunk-sort.d.ts new file mode 100644 index 000000000..db870df74 --- /dev/null +++ b/src/utils/package-chunk-sort.d.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { ScriptElement, StyleElement } from '../builders/browser/schema'; +export type EntryPointsType = [name: string, isModule: boolean]; +export declare function generateEntryPoints(options: { + styles: StyleElement[]; + scripts: ScriptElement[]; + isHMREnabled?: boolean; +}): EntryPointsType[]; diff --git a/src/utils/package-chunk-sort.js b/src/utils/package-chunk-sort.js new file mode 100644 index 000000000..de149a587 --- /dev/null +++ b/src/utils/package-chunk-sort.js @@ -0,0 +1,34 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.generateEntryPoints = generateEntryPoints; +const helpers_1 = require("../tools/webpack/utils/helpers"); +function generateEntryPoints(options) { + // Add all styles/scripts, except lazy-loaded ones. + const extraEntryPoints = (extraEntryPoints, defaultBundleName) => { + const entryPoints = (0, helpers_1.normalizeExtraEntryPoints)(extraEntryPoints, defaultBundleName) + .filter((entry) => entry.inject) + .map((entry) => entry.bundleName); + // remove duplicates + return [...new Set(entryPoints)].map((f) => [f, false]); + }; + const entryPoints = [ + ['runtime', !options.isHMREnabled], + ['polyfills', true], + ...extraEntryPoints(options.styles, 'styles'), + ...extraEntryPoints(options.scripts, 'scripts'), + ['vendor', true], + ['main', true], + ]; + const duplicates = entryPoints.filter(([name]) => entryPoints[0].indexOf(name) !== entryPoints[0].lastIndexOf(name)); + if (duplicates.length > 0) { + throw new Error(`Multiple bundles have been named the same: '${duplicates.join(`', '`)}'.`); + } + return entryPoints; +} diff --git a/src/utils/package-version.d.ts b/src/utils/package-version.d.ts new file mode 100644 index 000000000..0bda5a4e8 --- /dev/null +++ b/src/utils/package-version.d.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export declare const VERSION: string; diff --git a/src/utils/package-version.js b/src/utils/package-version.js new file mode 100644 index 000000000..b814bd4a6 --- /dev/null +++ b/src/utils/package-version.js @@ -0,0 +1,11 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VERSION = void 0; +exports.VERSION = require('../../package.json').version; diff --git a/src/utils/process-bundle-bootstrap.js b/src/utils/process-bundle-bootstrap.js deleted file mode 100644 index b2b1965f4..000000000 --- a/src/utils/process-bundle-bootstrap.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -require('../../../../../lib/bootstrap-local'); -module.exports = require('./process-bundle.ts'); \ No newline at end of file diff --git a/src/utils/process-bundle.d.ts b/src/utils/process-bundle.d.ts index ca1878157..ade42500a 100644 --- a/src/utils/process-bundle.d.ts +++ b/src/utils/process-bundle.d.ts @@ -1,80 +1,22 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { PluginObj } from '@babel/core'; -import { I18nOptions } from './i18n-options'; -export interface ProcessBundleOptions { - filename: string; - code: string; - map?: string; - name: string; - sourceMaps?: boolean; - hiddenSourceMaps?: boolean; - vendorSourceMaps?: boolean; - runtime?: boolean; - optimize?: boolean; - optimizeOnly?: boolean; - ignoreOriginal?: boolean; - cacheKeys?: (string | undefined)[]; - integrityAlgorithm?: 'sha256' | 'sha384' | 'sha512'; - runtimeData?: ProcessBundleResult[]; - replacements?: [string, string][]; - supportedBrowsers?: string[] | Record; -} -export interface ProcessBundleResult { - name: string; - integrity?: string; - original?: ProcessBundleFile; - downlevel?: ProcessBundleFile; -} -export interface ProcessBundleFile { - filename: string; - size: number; - integrity?: string; - map?: { - filename: string; - size: number; - }; -} -export declare const enum CacheKey { - OriginalCode = 0, - OriginalMap = 1, - DownlevelCode = 2, - DownlevelMap = 3 -} -export declare function setup(data: number[] | { - cachePath: string; - i18n: I18nOptions; -}): void; -export declare function process(options: ProcessBundleOptions): Promise; -export declare function createI18nPlugins(locale: string, translation: unknown | undefined, missingTranslation: 'error' | 'warning' | 'ignore', localeDataContent?: string): Promise<{ - diagnostics: import("@angular/localize/src/tools/src/diagnostics").Diagnostics; - plugins: PluginObj<{}>[]; -}>; -export interface InlineOptions { - filename: string; - code: string; - map?: string; - es5: boolean; - outputPath: string; - missingTranslation?: 'warning' | 'error' | 'ignore'; - setLocale?: boolean; -} +import { InlineOptions } from './bundle-inline-options'; export declare function inlineLocales(options: InlineOptions): Promise<{ file: string; diagnostics: { - type: "error" | "warning"; + type: "warning" | "error"; message: string; }[]; count: number; } | { file: string; diagnostics: { - type: "error" | "warning"; + type: "warning" | "error"; message: string; }[]; }>; diff --git a/src/utils/process-bundle.js b/src/utils/process-bundle.js index 516b9b16f..5ecf5b8b2 100644 --- a/src/utils/process-bundle.js +++ b/src/utils/process-bundle.js @@ -1,381 +1,111 @@ "use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.inlineLocales = exports.createI18nPlugins = exports.process = exports.setup = void 0; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -const core_1 = require("@babel/core"); -const template_1 = require("@babel/template"); -const crypto_1 = require("crypto"); -const fs = require("fs"); -const path = require("path"); -const source_map_1 = require("source-map"); -const terser_1 = require("terser"); -const v8 = require("v8"); -const webpack_sources_1 = require("webpack-sources"); -const environment_options_1 = require("./environment-options"); -const cacache = require('cacache'); -const deserialize = v8.deserialize; -// If code size is larger than 500KB, consider lower fidelity but faster sourcemap merge -const FAST_SOURCEMAP_THRESHOLD = 500 * 1024; -let cachePath; -let i18n; -function setup(data) { - const options = Array.isArray(data) - ? deserialize(Buffer.from(data)) - : data; - cachePath = options.cachePath; - i18n = options.i18n; -} -exports.setup = setup; -async function cachePut(content, key, integrity) { - if (cachePath && key) { - await cacache.put(cachePath, key || null, content, { - metadata: { integrity }, - }); - } -} -async function process(options) { - if (!options.cacheKeys) { - options.cacheKeys = []; - } - const result = { name: options.name }; - if (options.integrityAlgorithm) { - // Store unmodified code integrity value -- used for SRI value replacement - result.integrity = generateIntegrityValue(options.integrityAlgorithm, options.code); - } - // Runtime chunk requires specialized handling - if (options.runtime) { - return { ...result, ...(await processRuntime(options)) }; - } - const basePath = path.dirname(options.filename); - const filename = path.basename(options.filename); - const downlevelFilename = filename.replace(/\-(es20\d{2}|esnext)/, '-es5'); - const downlevel = !options.optimizeOnly; - const sourceCode = options.code; - const sourceMap = options.map ? JSON.parse(options.map) : undefined; - let downlevelCode; - let downlevelMap; - if (downlevel) { - const { supportedBrowsers: targets = [] } = options; - // todo: revisit this in version 10, when we update our defaults browserslist - // Without this workaround bundles will not be downlevelled because Babel doesn't know handle to 'op_mini all' - // See: https://github.com/babel/babel/issues/11155 - if (Array.isArray(targets) && targets.includes('op_mini all')) { - targets.push('ie_mob 11'); - } - else if ('op_mini' in targets) { - targets['ie_mob'] = '11'; - } - // Downlevel the bundle - const transformResult = await core_1.transformAsync(sourceCode, { - filename, - // using false ensures that babel will NOT search and process sourcemap comments (large memory usage) - // The types do not include the false option even though it is valid - // tslint:disable-next-line: no-any - inputSourceMap: false, - babelrc: false, - configFile: false, - presets: [[ - require.resolve('@babel/preset-env'), - { - // browserslist-compatible query or object of minimum environment versions to support - targets, - // modules aren't needed since the bundles use webpack's custom module loading - modules: false, - // 'transform-typeof-symbol' generates slower code - exclude: ['transform-typeof-symbol'], - }, - ]], - plugins: options.replacements ? [createReplacePlugin(options.replacements)] : [], - minified: environment_options_1.allowMinify && !!options.optimize, - compact: !environment_options_1.shouldBeautify && !!options.optimize, - sourceMaps: !!sourceMap, - }); - if (!transformResult || !transformResult.code) { - throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`); - } - downlevelCode = transformResult.code; - if (sourceMap && transformResult.map) { - // String length is used as an estimate for byte length - const fastSourceMaps = sourceCode.length > FAST_SOURCEMAP_THRESHOLD; - downlevelMap = await mergeSourceMaps(sourceCode, sourceMap, downlevelCode, transformResult.map, filename, - // When not optimizing, the sourcemaps are significantly less complex - // and can use the higher fidelity merge - !!options.optimize && fastSourceMaps); - } - } - if (downlevelCode) { - result.downlevel = await processBundle({ - ...options, - code: downlevelCode, - map: downlevelMap, - filename: path.join(basePath, downlevelFilename), - isOriginal: false, - }); - } - if (!result.original && !options.ignoreOriginal) { - result.original = await processBundle({ - ...options, - isOriginal: true, - }); - } - return result; -} -exports.process = process; -async function mergeSourceMaps(inputCode, inputSourceMap, resultCode, resultSourceMap, filename, fast = false) { - if (fast) { - return mergeSourceMapsFast(inputSourceMap, resultSourceMap); - } - // SourceMapSource produces high-quality sourcemaps - // The last argument is not yet in the typings - // tslint:disable-next-line: no-any - return new webpack_sources_1.SourceMapSource(resultCode, filename, resultSourceMap, inputCode, inputSourceMap, true).map(); -} -async function mergeSourceMapsFast(first, second) { - const sourceRoot = first.sourceRoot; - const generator = new source_map_1.SourceMapGenerator(); - // sourcemap package adds the sourceRoot to all position source paths if not removed - delete first.sourceRoot; - await source_map_1.SourceMapConsumer.with(first, null, originalConsumer => { - return source_map_1.SourceMapConsumer.with(second, null, newConsumer => { - newConsumer.eachMapping(mapping => { - if (mapping.originalLine === null) { - return; - } - const originalPosition = originalConsumer.originalPositionFor({ - line: mapping.originalLine, - column: mapping.originalColumn, - }); - if (originalPosition.line === null || - originalPosition.column === null || - originalPosition.source === null) { - return; - } - generator.addMapping({ - generated: { - line: mapping.generatedLine, - column: mapping.generatedColumn, - }, - name: originalPosition.name || undefined, - original: { - line: originalPosition.line, - column: originalPosition.column, - }, - source: originalPosition.source, - }); - }); - }); - }); - const map = generator.toJSON(); - map.file = second.file; - map.sourceRoot = sourceRoot; - // Add source content if present - if (first.sourcesContent) { - // Source content array is based on index of sources - const sourceContentMap = new Map(); - for (let i = 0; i < first.sources.length; i++) { - // make paths "absolute" so they can be compared (`./a.js` and `a.js` are equivalent) - sourceContentMap.set(path.resolve('/', first.sources[i]), i); - } - map.sourcesContent = []; - for (let i = 0; i < map.sources.length; i++) { - const contentIndex = sourceContentMap.get(path.resolve('/', map.sources[i])); - if (contentIndex === undefined) { - map.sourcesContent.push(''); - } - else { - map.sourcesContent.push(first.sourcesContent[contentIndex]); - } - } - } - // Put the sourceRoot back - if (sourceRoot) { - first.sourceRoot = sourceRoot; - } - return map; -} -async function processBundle(options) { - const { optimize, isOriginal, code, map, filename: filepath, hiddenSourceMaps, cacheKeys = [], integrityAlgorithm, } = options; - const rawMap = typeof map === 'string' ? JSON.parse(map) : map; - const filename = path.basename(filepath); - let result; - if (rawMap) { - rawMap.file = filename; - } - if (optimize) { - result = await terserMangle(code, { - filename, - map: rawMap, - compress: !isOriginal, - ecma: isOriginal ? 6 : 5, - }); - } - else { - result = { - map: rawMap, - code, +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; }; - } - let mapContent; - if (result.map) { - if (!hiddenSourceMaps) { - result.code += `\n//# sourceMappingURL=${filename}.map`; - } - mapContent = JSON.stringify(result.map); - await cachePut(mapContent, cacheKeys[isOriginal ? 1 /* OriginalMap */ : 3 /* DownlevelMap */]); - fs.writeFileSync(filepath + '.map', mapContent); - } - const fileResult = createFileEntry(filepath, result.code, mapContent, integrityAlgorithm); - await cachePut(result.code, cacheKeys[isOriginal ? 0 /* OriginalCode */ : 2 /* DownlevelCode */], fileResult.integrity); - fs.writeFileSync(filepath, result.code); - return fileResult; -} -async function terserMangle(code, options = {}) { - // Note: Investigate converting the AST instead of re-parsing - // estree -> terser is already supported; need babel -> estree/terser - // Mangle downlevel code - const minifyOutput = terser_1.minify(options.filename ? { [options.filename]: code } : code, { - compress: environment_options_1.allowMinify && !!options.compress, - ecma: options.ecma || 5, - mangle: environment_options_1.allowMangle, - safari10: true, - output: { - ascii_only: true, - webkit: true, - beautify: environment_options_1.shouldBeautify, - }, - sourceMap: !!options.map && - { - asObject: true, - }, - }); - if (minifyOutput.error) { - throw minifyOutput.error; - } - // tslint:disable-next-line: no-non-null-assertion - const outputCode = minifyOutput.code; - let outputMap; - if (options.map && minifyOutput.map) { - outputMap = await mergeSourceMaps(code, options.map, outputCode, minifyOutput.map, options.filename || '0', code.length > FAST_SOURCEMAP_THRESHOLD); - } - return { code: outputCode, map: outputMap }; -} -function createFileEntry(filename, code, map, integrityAlgorithm) { - return { - filename: filename, - size: Buffer.byteLength(code), - integrity: integrityAlgorithm && generateIntegrityValue(integrityAlgorithm, code), - map: !map - ? undefined - : { - filename: filename + '.map', - size: Buffer.byteLength(map), - }, + return ownKeys(o); }; -} -function generateIntegrityValue(hashAlgorithm, code) { - return (hashAlgorithm + - '-' + - crypto_1.createHash(hashAlgorithm) - .update(code) - .digest('base64')); -} -// The webpack runtime chunk is already ES5. -// However, two variants are still needed due to lazy routing and SRI differences -// NOTE: This should eventually be a babel plugin -async function processRuntime(options) { - let originalCode = options.code; - let downlevelCode = options.code; - // Replace integrity hashes with updated values - if (options.integrityAlgorithm && options.runtimeData) { - for (const data of options.runtimeData) { - if (!data.integrity) { - continue; - } - if (data.original && data.original.integrity) { - originalCode = originalCode.replace(data.integrity, data.original.integrity); - } - if (data.downlevel && data.downlevel.integrity) { - downlevelCode = downlevelCode.replace(data.integrity, data.downlevel.integrity); - } - } - } - // Adjust lazy loaded scripts to point to the proper variant - // Extra spacing is intentional to align source line positions - downlevelCode = downlevelCode.replace(/"\-(es20\d{2}|esnext)\./, ' "-es5.'); - return { - original: await processBundle({ - ...options, - code: originalCode, - isOriginal: true, - }), - downlevel: await processBundle({ - ...options, - code: downlevelCode, - filename: options.filename.replace(/\-(es20\d{2}|esnext)/, '-es5'), - isOriginal: false, - }), + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; }; -} -function createReplacePlugin(replacements) { - return { - visitor: { - StringLiteral(path) { - for (const replacement of replacements) { - if (path.node.value === replacement[0]) { - path.node.value = replacement[1]; - } - } - }, - }, - }; -} +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.inlineLocales = inlineLocales; +const remapping_1 = __importDefault(require("@ampproject/remapping")); +const core_1 = require("@babel/core"); +const fs = __importStar(require("node:fs/promises")); +const path = __importStar(require("node:path")); +const node_worker_threads_1 = require("node:worker_threads"); +const environment_options_1 = require("./environment-options"); +const error_1 = require("./error"); +const load_esm_1 = require("./load-esm"); +// Lazy loaded webpack-sources object +// Webpack is only imported if needed during the processing +let webpackSources; +const { i18n } = (node_worker_threads_1.workerData || {}); +/** + * Internal flag to enable the direct usage of the `@angular/localize` translation plugins. + * Their usage is currently several times slower than the string manipulation method. + * Future work to optimize the plugins should enable plugin usage as the default. + */ const USE_LOCALIZE_PLUGINS = false; -async function createI18nPlugins(locale, translation, missingTranslation, localeDataContent) { +/** + * Cached instance of the `@angular/localize/tools` module. + * This is used to remove the need to repeatedly import the module per file translation. + */ +let localizeToolsModule; +/** + * Attempts to load the `@angular/localize/tools` module containing the functionality to + * perform the file translations. + * This module must be dynamically loaded as it is an ESM module and this file is CommonJS. + */ +async function loadLocalizeTools() { + if (localizeToolsModule !== undefined) { + return localizeToolsModule; + } + // Load ESM `@angular/localize/tools` using the TypeScript dynamic import workaround. + // Once TypeScript provides support for keeping the dynamic import this workaround can be + // changed to a direct dynamic import. + return (0, load_esm_1.loadEsmModule)('@angular/localize/tools'); +} +async function createI18nPlugins(locale, translation, missingTranslation, shouldInline, localeDataContent) { + const { Diagnostics, makeEs2015TranslatePlugin, makeLocalePlugin } = await loadLocalizeTools(); const plugins = []; - // tslint:disable-next-line: no-implicit-dependencies - const localizeDiag = await Promise.resolve().then(() => require('@angular/localize/src/tools/src/diagnostics')); - const diagnostics = new localizeDiag.Diagnostics(); - const es2015 = await Promise.resolve().then(() => require( - // tslint:disable-next-line: trailing-comma no-implicit-dependencies - '@angular/localize/src/tools/src/translate/source_files/es2015_translate_plugin')); - plugins.push( - // tslint:disable-next-line: no-any - es2015.makeEs2015TranslatePlugin(diagnostics, (translation || {}), { - missingTranslation: translation === undefined ? 'ignore' : missingTranslation, - })); - const es5 = await Promise.resolve().then(() => require( - // tslint:disable-next-line: trailing-comma no-implicit-dependencies - '@angular/localize/src/tools/src/translate/source_files/es5_translate_plugin')); - plugins.push( - // tslint:disable-next-line: no-any - es5.makeEs5TranslatePlugin(diagnostics, (translation || {}), { - missingTranslation: translation === undefined ? 'ignore' : missingTranslation, - })); - const inlineLocale = await Promise.resolve().then(() => require( - // tslint:disable-next-line: trailing-comma no-implicit-dependencies - '@angular/localize/src/tools/src/translate/source_files/locale_plugin')); - plugins.push(inlineLocale.makeLocalePlugin(locale)); + const diagnostics = new Diagnostics(); + if (shouldInline) { + plugins.push( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + makeEs2015TranslatePlugin(diagnostics, (translation || {}), { + missingTranslation: translation === undefined ? 'ignore' : missingTranslation, + })); + } + plugins.push(makeLocalePlugin(locale)); if (localeDataContent) { plugins.push({ visitor: { Program(path) { - path.unshiftContainer('body', template_1.default.ast(localeDataContent)); + path.unshiftContainer('body', core_1.template.ast(localeDataContent)); }, }, }); } return { diagnostics, plugins }; } -exports.createI18nPlugins = createI18nPlugins; const localizeName = '$localize'; async function inlineLocales(options) { - var _a; if (!i18n || i18n.inlineLocales.size === 0) { return { file: options.filename, diagnostics: [], count: 0 }; } @@ -386,24 +116,24 @@ async function inlineLocales(options) { if (!hasLocalizeName && !options.setLocale) { return inlineCopyOnly(options); } + await loadLocalizeTools(); let ast; try { - ast = core_1.parseSync(options.code, { + ast = (0, core_1.parseSync)(options.code, { babelrc: false, configFile: false, - sourceType: 'script', + sourceType: 'unambiguous', filename: options.filename, }); } catch (error) { - if (error.message) { - // Make the error more readable. - // Same errors will contain the full content of the file as the error message - // Which makes it hard to find the actual error message. - const index = error.message.indexOf(')\n'); - const msg = index !== -1 ? error.message.substr(0, index + 1) : error.message; - throw new Error(`${msg}\nAn error occurred inlining file "${options.filename}"`); - } + (0, error_1.assertIsError)(error); + // Make the error more readable. + // Same errors will contain the full content of the file as the error message + // Which makes it hard to find the actual error message. + const index = error.message.indexOf(')\n'); + const msg = index !== -1 ? error.message.slice(0, index + 1) : error.message; + throw new Error(`${msg}\nAn error occurred inlining file "${options.filename}"`); } if (!ast) { throw new Error(`Unknown error occurred inlining file "${options.filename}"`); @@ -412,174 +142,159 @@ async function inlineLocales(options) { return inlineLocalesDirect(ast, options); } const diagnostics = []; - const inputMap = options.map && JSON.parse(options.map); for (const locale of i18n.inlineLocales) { const isSourceLocale = locale === i18n.sourceLocale; - // tslint:disable-next-line: no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const translations = isSourceLocale ? {} : i18n.locales[locale].translation || {}; let localeDataContent; if (options.setLocale) { // If locale data is provided, load it and prepend to file - const localeDataPath = (_a = i18n.locales[locale]) === null || _a === void 0 ? void 0 : _a.dataPath; + const localeDataPath = i18n.locales[locale]?.dataPath; if (localeDataPath) { - localeDataContent = await loadLocaleData(localeDataPath, true, options.es5); + localeDataContent = await loadLocaleData(localeDataPath, true); } } - const { diagnostics: localeDiagnostics, plugins } = await createI18nPlugins(locale, translations, isSourceLocale ? 'ignore' : options.missingTranslation || 'warning', localeDataContent); - const transformResult = await core_1.transformFromAstSync(ast, options.code, { + const { diagnostics: localeDiagnostics, plugins } = await createI18nPlugins(locale, translations, isSourceLocale ? 'ignore' : options.missingTranslation || 'warning', true, localeDataContent); + const transformResult = (0, core_1.transformFromAstSync)(ast, options.code, { filename: options.filename, // using false ensures that babel will NOT search and process sourcemap comments (large memory usage) // The types do not include the false option even though it is valid - // tslint:disable-next-line: no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any inputSourceMap: false, babelrc: false, configFile: false, plugins, compact: !environment_options_1.shouldBeautify, - sourceMaps: !!inputMap, + sourceMaps: !!options.map, }); diagnostics.push(...localeDiagnostics.messages); if (!transformResult || !transformResult.code) { throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`); } - const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : locale, options.filename); - fs.writeFileSync(outputPath, transformResult.code); - if (inputMap && transformResult.map) { - const outputMap = await mergeSourceMaps(options.code, inputMap, transformResult.code, transformResult.map, options.filename, options.code.length > FAST_SOURCEMAP_THRESHOLD); - fs.writeFileSync(outputPath + '.map', JSON.stringify(outputMap)); + const subPath = i18n.locales[locale].subPath; + const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : subPath, options.filename); + await fs.writeFile(outputPath, transformResult.code); + if (options.map && transformResult.map) { + const outputMap = (0, remapping_1.default)([transformResult.map, options.map], () => null); + await fs.writeFile(outputPath + '.map', JSON.stringify(outputMap)); } } return { file: options.filename, diagnostics }; } -exports.inlineLocales = inlineLocales; async function inlineLocalesDirect(ast, options) { if (!i18n || i18n.inlineLocales.size === 0) { return { file: options.filename, diagnostics: [], count: 0 }; } - const { default: generate } = await Promise.resolve().then(() => require('@babel/generator')); - // tslint:disable-next-line: no-implicit-dependencies - const utils = await Promise.resolve().then(() => require('@angular/localize/src/tools/src/source_file_utils')); - // tslint:disable-next-line: no-implicit-dependencies - const localizeDiag = await Promise.resolve().then(() => require('@angular/localize/src/tools/src/diagnostics')); + const { default: generate } = await Promise.resolve().then(() => __importStar(require('@babel/generator'))); + const localizeDiag = await loadLocalizeTools(); const diagnostics = new localizeDiag.Diagnostics(); - const positions = findLocalizePositions(ast, options, utils); + const positions = findLocalizePositions(ast, options, localizeDiag); if (positions.length === 0 && !options.setLocale) { return inlineCopyOnly(options); } - const inputMap = options.map && JSON.parse(options.map); + const inputMap = !!options.map && JSON.parse(options.map); // Cleanup source root otherwise it will be added to each source entry const mapSourceRoot = inputMap && inputMap.sourceRoot; if (inputMap) { delete inputMap.sourceRoot; } + // Load Webpack only when needed + if (webpackSources === undefined) { + webpackSources = (await Promise.resolve().then(() => __importStar(require('webpack')))).sources; + } + const { ConcatSource, OriginalSource, ReplaceSource, SourceMapSource } = webpackSources; for (const locale of i18n.inlineLocales) { - const content = new webpack_sources_1.ReplaceSource(inputMap - ? // tslint:disable-next-line: no-any - new webpack_sources_1.SourceMapSource(options.code, options.filename, inputMap) - : new webpack_sources_1.OriginalSource(options.code, options.filename)); + const content = new ReplaceSource(inputMap + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + new SourceMapSource(options.code, options.filename, inputMap) + : new OriginalSource(options.code, options.filename)); const isSourceLocale = locale === i18n.sourceLocale; - // tslint:disable-next-line: no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const translations = isSourceLocale ? {} : i18n.locales[locale].translation || {}; for (const position of positions) { - const translated = utils.translate(diagnostics, translations, position.messageParts, position.expressions, isSourceLocale ? 'ignore' : options.missingTranslation || 'warning'); - const expression = utils.buildLocalizeReplacement(translated[0], translated[1]); + const translated = localizeDiag.translate(diagnostics, translations, position.messageParts, position.expressions, isSourceLocale ? 'ignore' : options.missingTranslation || 'warning'); + const expression = localizeDiag.buildLocalizeReplacement(translated[0], translated[1]); const { code } = generate(expression); content.replace(position.start, position.end - 1, code); } let outputSource = content; if (options.setLocale) { - const setLocaleText = `var $localize=Object.assign(void 0===$localize?{}:$localize,{locale:"${locale}"});\n`; + const setLocaleText = `globalThis.$localize=Object.assign(globalThis.$localize || {},{locale:"${locale}"});\n`; // If locale data is provided, load it and prepend to file - let localeDataSource = null; + let localeDataSource; const localeDataPath = i18n.locales[locale] && i18n.locales[locale].dataPath; if (localeDataPath) { - const localeDataContent = await loadLocaleData(localeDataPath, true, options.es5); - localeDataSource = new webpack_sources_1.OriginalSource(localeDataContent, path.basename(localeDataPath)); + const localeDataContent = await loadLocaleData(localeDataPath, true); + localeDataSource = new OriginalSource(localeDataContent, path.basename(localeDataPath)); } outputSource = localeDataSource - // The semicolon ensures that there is no syntax error between statements - ? new webpack_sources_1.ConcatSource(setLocaleText, localeDataSource, ';\n', content) - : new webpack_sources_1.ConcatSource(setLocaleText, content); + ? // The semicolon ensures that there is no syntax error between statements + new ConcatSource(setLocaleText, localeDataSource, ';\n', content) + : new ConcatSource(setLocaleText, content); } const { source: outputCode, map: outputMap } = outputSource.sourceAndMap(); - const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : locale, options.filename); - fs.writeFileSync(outputPath, outputCode); + const subPath = i18n.locales[locale].subPath; + const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : subPath, options.filename); + await fs.writeFile(outputPath, outputCode); if (inputMap && outputMap) { outputMap.file = options.filename; if (mapSourceRoot) { outputMap.sourceRoot = mapSourceRoot; } - fs.writeFileSync(outputPath + '.map', JSON.stringify(outputMap)); + await fs.writeFile(outputPath + '.map', JSON.stringify(outputMap)); } } return { file: options.filename, diagnostics: diagnostics.messages, count: positions.length }; } -function inlineCopyOnly(options) { +async function inlineCopyOnly(options) { if (!i18n) { throw new Error('i18n options are missing'); } for (const locale of i18n.inlineLocales) { - const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : locale, options.filename); - fs.writeFileSync(outputPath, options.code); + const subPath = i18n.locales[locale].subPath; + const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : subPath, options.filename); + await fs.writeFile(outputPath, options.code); if (options.map) { - fs.writeFileSync(outputPath + '.map', options.map); + await fs.writeFile(outputPath + '.map', options.map); } } return { file: options.filename, diagnostics: [], count: 0 }; } -function findLocalizePositions(ast, options, -// tslint:disable-next-line: no-implicit-dependencies -utils) { +function findLocalizePositions(ast, options, utils) { const positions = []; - if (options.es5) { - core_1.traverse(ast, { - CallExpression(path) { - const callee = path.get('callee'); - if (callee.isIdentifier() && - callee.node.name === localizeName && - utils.isGlobalIdentifier(callee)) { - const messageParts = utils.unwrapMessagePartsFromLocalizeCall(path); - const expressions = utils.unwrapSubstitutionsFromLocalizeCall(path.node); - positions.push({ - // tslint:disable-next-line: no-non-null-assertion - start: path.node.start, - // tslint:disable-next-line: no-non-null-assertion - end: path.node.end, - messageParts, - expressions, - }); - } - }, - }); - } - else { - const traverseFast = core_1.types.traverseFast; - traverseFast(ast, node => { - if (node.type === 'TaggedTemplateExpression' && - core_1.types.isIdentifier(node.tag) && - node.tag.name === localizeName) { - const messageParts = utils.unwrapMessagePartsFromTemplateLiteral(node.quasi.quasis); + // Workaround to ensure a path hub is present for traversal + const { File } = require('@babel/core'); + const file = new File({}, { code: options.code, ast }); + (0, core_1.traverse)(file.ast, { + TaggedTemplateExpression(path) { + if (core_1.types.isIdentifier(path.node.tag) && path.node.tag.name === localizeName) { + const [messageParts, expressions] = unwrapTemplateLiteral(path, utils); positions.push({ - // tslint:disable-next-line: no-non-null-assertion - start: node.start, - // tslint:disable-next-line: no-non-null-assertion - end: node.end, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + start: path.node.start, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + end: path.node.end, messageParts, - expressions: node.quasi.expressions, + expressions, }); } - }); - } + }, + }); return positions; } -async function loadLocaleData(path, optimize, es5) { +function unwrapTemplateLiteral(path, utils) { + const [messageParts] = utils.unwrapMessagePartsFromTemplateLiteral(path.get('quasi').get('quasis')); + const [expressions] = utils.unwrapExpressionsFromTemplateLiteral(path.get('quasi')); + return [messageParts, expressions]; +} +async function loadLocaleData(path, optimize) { // The path is validated during option processing before the build starts - const content = fs.readFileSync(path, 'utf8'); + const content = await fs.readFile(path, 'utf8'); // Downlevel and optimize the data - const transformResult = await core_1.transformAsync(content, { + const transformResult = await (0, core_1.transformAsync)(content, { filename: path, // The types do not include the false option even though it is valid - // tslint:disable-next-line: no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any inputSourceMap: false, babelrc: false, configFile: false, @@ -588,8 +303,7 @@ async function loadLocaleData(path, optimize, es5) { require.resolve('@babel/preset-env'), { bugfixes: true, - // IE 9 is the oldest supported browser - targets: es5 ? { ie: '9' } : { esmodules: true }, + targets: { esmodules: true }, }, ], ], diff --git a/src/angular-cli-files/utilities/read-tsconfig.d.ts b/src/utils/read-tsconfig.d.ts similarity index 66% rename from src/angular-cli-files/utilities/read-tsconfig.d.ts rename to src/utils/read-tsconfig.d.ts index 400a10cc7..ddae064fb 100644 --- a/src/angular-cli-files/utilities/read-tsconfig.d.ts +++ b/src/utils/read-tsconfig.d.ts @@ -1,11 +1,11 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { ParsedConfiguration } from '@angular/compiler-cli'; +import type { ParsedConfiguration } from '@angular/compiler-cli'; /** * Reads and parses a given TsConfig file. * @@ -13,4 +13,4 @@ import { ParsedConfiguration } from '@angular/compiler-cli'; * @param workspaceRoot - workspaceRoot root location when provided * it will resolve 'tsconfigPath' from this path. */ -export declare function readTsconfig(tsconfigPath: string, workspaceRoot?: string): ParsedConfiguration; +export declare function readTsconfig(tsconfigPath: string, workspaceRoot?: string): Promise; diff --git a/src/utils/read-tsconfig.js b/src/utils/read-tsconfig.js new file mode 100644 index 000000000..bf246fe2f --- /dev/null +++ b/src/utils/read-tsconfig.js @@ -0,0 +1,64 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.readTsconfig = readTsconfig; +const path = __importStar(require("node:path")); +const load_esm_1 = require("./load-esm"); +/** + * Reads and parses a given TsConfig file. + * + * @param tsconfigPath - An absolute or relative path from 'workspaceRoot' of the tsconfig file. + * @param workspaceRoot - workspaceRoot root location when provided + * it will resolve 'tsconfigPath' from this path. + */ +async function readTsconfig(tsconfigPath, workspaceRoot) { + const tsConfigFullPath = workspaceRoot ? path.resolve(workspaceRoot, tsconfigPath) : tsconfigPath; + // Load ESM `@angular/compiler-cli` using the TypeScript dynamic import workaround. + // Once TypeScript provides support for keeping the dynamic import this workaround can be + // changed to a direct dynamic import. + const { formatDiagnostics, readConfiguration } = await (0, load_esm_1.loadEsmModule)('@angular/compiler-cli'); + const configResult = readConfiguration(tsConfigFullPath); + if (configResult.errors && configResult.errors.length) { + throw new Error(formatDiagnostics(configResult.errors)); + } + return configResult; +} diff --git a/src/utils/run-module-as-observable-fork.d.ts b/src/utils/run-module-as-observable-fork.d.ts index b256ba612..567269164 100644 --- a/src/utils/run-module-as-observable-fork.d.ts +++ b/src/utils/run-module-as-observable-fork.d.ts @@ -1,9 +1,9 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { BuilderOutput } from '@angular-devkit/architect'; import { Observable } from 'rxjs'; diff --git a/src/utils/run-module-as-observable-fork.js b/src/utils/run-module-as-observable-fork.js index 711df74f3..ca71ad05f 100644 --- a/src/utils/run-module-as-observable-fork.js +++ b/src/utils/run-module-as-observable-fork.js @@ -1,15 +1,25 @@ "use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -exports.runModuleAsObservableFork = void 0; -const child_process_1 = require("child_process"); -const path_1 = require("path"); +exports.runModuleAsObservableFork = runModuleAsObservableFork; +const node_child_process_1 = require("node:child_process"); +const node_path_1 = require("node:path"); const rxjs_1 = require("rxjs"); -const treeKill = require('tree-kill'); +const tree_kill_1 = __importDefault(require("tree-kill")); function runModuleAsObservableFork(cwd, modulePath, exportName, -// tslint:disable-next-line:no-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any args) { - return new rxjs_1.Observable(obs => { - const workerPath = path_1.resolve(__dirname, './run-module-worker.js'); + return new rxjs_1.Observable((obs) => { + const workerPath = (0, node_path_1.resolve)(__dirname, './run-module-worker.js'); const debugArgRegex = /--inspect(?:-brk|-port)?|--debug(?:-brk|-port)/; const execArgv = process.execArgv.filter((arg) => { // Remove debug args. @@ -28,11 +38,11 @@ args) { // logger.error, // make it a stream // ]; // } - const forkedProcess = child_process_1.fork(workerPath, undefined, forkOptions); + const forkedProcess = (0, node_child_process_1.fork)(workerPath, undefined, forkOptions); // Cleanup. const killForkedProcess = () => { if (forkedProcess && forkedProcess.pid) { - treeKill(forkedProcess.pid, 'SIGTERM'); + (0, tree_kill_1.default)(forkedProcess.pid, 'SIGTERM'); } }; // Handle child process exit. @@ -65,4 +75,3 @@ args) { return killForkedProcess; }); } -exports.runModuleAsObservableFork = runModuleAsObservableFork; diff --git a/src/utils/run-module-worker.js b/src/utils/run-module-worker.js index 2370abead..a6f3fa545 100644 --- a/src/utils/run-module-worker.js +++ b/src/utils/run-module-worker.js @@ -1,11 +1,10 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ - process.on('message', (message) => { // Only process messages with the hash in 'run-module-as-observable-fork.ts'. if (message.hash === '5d4b9a5c0a4e0f9977598437b0e85bcc') { @@ -17,4 +16,3 @@ process.on('message', (message) => { } } }); - diff --git a/src/utils/spinner.d.ts b/src/utils/spinner.d.ts new file mode 100644 index 000000000..0a90f9de4 --- /dev/null +++ b/src/utils/spinner.d.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export declare class Spinner { + #private; + private readonly spinner; + /** When false, only fail messages will be displayed. */ + enabled: boolean; + constructor(text?: string); + set text(text: string); + get isSpinning(): boolean; + succeed(text?: string): void; + fail(text?: string): void; + stop(): void; + start(text?: string): void; +} diff --git a/src/utils/spinner.js b/src/utils/spinner.js new file mode 100644 index 000000000..e9beebb07 --- /dev/null +++ b/src/utils/spinner.js @@ -0,0 +1,55 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Spinner = void 0; +const ora_1 = __importDefault(require("ora")); +const color_1 = require("./color"); +const tty_1 = require("./tty"); +class Spinner { + spinner; + /** When false, only fail messages will be displayed. */ + enabled = true; + #isTTY = (0, tty_1.isTTY)(); + constructor(text) { + this.spinner = (0, ora_1.default)({ + text: text === undefined ? undefined : text + '\n', + // The below 2 options are needed because otherwise CTRL+C will be delayed + // when the underlying process is sync. + hideCursor: false, + discardStdin: false, + isEnabled: this.#isTTY, + }); + } + set text(text) { + this.spinner.text = text; + } + get isSpinning() { + return this.spinner.isSpinning || !this.#isTTY; + } + succeed(text) { + if (this.enabled) { + this.spinner.succeed(text); + } + } + fail(text) { + this.spinner.fail(text && color_1.colors.redBright(text)); + } + stop() { + this.spinner.stop(); + } + start(text) { + if (this.enabled) { + this.spinner.start(text); + } + } +} +exports.Spinner = Spinner; diff --git a/src/utils/test-files.d.ts b/src/utils/test-files.d.ts new file mode 100644 index 000000000..e6d7da17e --- /dev/null +++ b/src/utils/test-files.d.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { glob as globFn } from 'tinyglobby'; +/** + * Finds all test files in the project. + * + * @param options The builder options describing where to find tests. + * @param workspaceRoot The path to the root directory of the workspace. + * @param glob A promisified implementation of the `glob` module. Only intended for + * testing purposes. + * @returns A set of all test files in the project. + */ +export declare function findTestFiles(include: string[], exclude: string[], workspaceRoot: string, glob?: typeof globFn): Promise>; diff --git a/src/utils/test-files.js b/src/utils/test-files.js new file mode 100644 index 000000000..551550059 --- /dev/null +++ b/src/utils/test-files.js @@ -0,0 +1,31 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.findTestFiles = findTestFiles; +const tinyglobby_1 = require("tinyglobby"); +/** + * Finds all test files in the project. + * + * @param options The builder options describing where to find tests. + * @param workspaceRoot The path to the root directory of the workspace. + * @param glob A promisified implementation of the `glob` module. Only intended for + * testing purposes. + * @returns A set of all test files in the project. + */ +async function findTestFiles(include, exclude, workspaceRoot, glob = tinyglobby_1.glob) { + const globOptions = { + cwd: workspaceRoot, + ignore: ['node_modules/**'].concat(exclude), + braceExpansion: false, // Do not expand `a{b,c}` to `ab,ac`. + extglob: false, // Disable "extglob" patterns. + }; + const included = await Promise.all(include.map((pattern) => glob(pattern, globOptions))); + // Flatten and deduplicate any files found in multiple include patterns. + return new Set(included.flat()); +} diff --git a/src/utils/tty.d.ts b/src/utils/tty.d.ts new file mode 100644 index 000000000..4b0b508a0 --- /dev/null +++ b/src/utils/tty.d.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +export declare function isTTY(): boolean; diff --git a/src/utils/tty.js b/src/utils/tty.js new file mode 100644 index 000000000..ac9b084f5 --- /dev/null +++ b/src/utils/tty.js @@ -0,0 +1,22 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isTTY = isTTY; +function _isTruthy(value) { + // Returns true if value is a string that is anything but 0 or false. + return value !== undefined && value !== '0' && value.toUpperCase() !== 'FALSE'; +} +function isTTY() { + // If we force TTY, we always return true. + const force = process.env['NG_FORCE_TTY']; + if (force !== undefined) { + return _isTruthy(force); + } + return !!process.stdout.isTTY && !_isTruthy(process.env['CI']); +} diff --git a/src/utils/url.d.ts b/src/utils/url.d.ts index c856ab482..d6e19160f 100644 --- a/src/utils/url.d.ts +++ b/src/utils/url.d.ts @@ -1,8 +1,8 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ export declare function urlJoin(...parts: string[]): string; diff --git a/src/utils/url.js b/src/utils/url.js index 40b26d89a..6674de1f2 100644 --- a/src/utils/url.js +++ b/src/utils/url.js @@ -1,13 +1,13 @@ "use strict"; /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.urlJoin = void 0; +exports.urlJoin = urlJoin; function urlJoin(...parts) { const [p, ...rest] = parts; // Remove trailing slash from first part @@ -15,4 +15,3 @@ function urlJoin(...parts) { // Dedupe double slashes from path names return p.replace(/\/$/, '') + ('/' + rest.join('/')).replace(/\/\/+/g, '/'); } -exports.urlJoin = urlJoin; diff --git a/src/utils/version.d.ts b/src/utils/version.d.ts deleted file mode 100644 index bbc9c656b..000000000 --- a/src/utils/version.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { logging } from '@angular-devkit/core'; -export declare function assertCompatibleAngularVersion(projectRoot: string, logger: logging.LoggerApi): void; diff --git a/src/utils/version.js b/src/utils/version.js deleted file mode 100644 index c2dec42d5..000000000 --- a/src/utils/version.js +++ /dev/null @@ -1,91 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.assertCompatibleAngularVersion = void 0; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const core_1 = require("@angular-devkit/core"); -const semver_1 = require("semver"); -function assertCompatibleAngularVersion(projectRoot, logger) { - let angularCliPkgJson; - let angularPkgJson; - let rxjsPkgJson; - const resolveOptions = { paths: [projectRoot] }; - try { - const angularPackagePath = require.resolve('@angular/core/package.json', resolveOptions); - const rxjsPackagePath = require.resolve('rxjs/package.json', resolveOptions); - angularPkgJson = require(angularPackagePath); - rxjsPkgJson = require(rxjsPackagePath); - } - catch (_a) { - logger.error(core_1.tags.stripIndents ` - You seem to not be depending on "@angular/core" and/or "rxjs". This is an error. - `); - process.exit(2); - } - if (!(angularPkgJson && angularPkgJson['version'] && rxjsPkgJson && rxjsPkgJson['version'])) { - logger.error(core_1.tags.stripIndents ` - Cannot determine versions of "@angular/core" and/or "rxjs". - This likely means your local installation is broken. Please reinstall your packages. - `); - process.exit(2); - } - try { - const angularCliPkgPath = require.resolve('@angular/cli/package.json', resolveOptions); - angularCliPkgJson = require(angularCliPkgPath); - if (!(angularCliPkgJson && angularCliPkgJson['version'])) { - throw new Error(); - } - } - catch (_b) { - // Not using @angular-devkit/build-angular with @angular/cli is ok too. - // In this case we don't provide as many version checks. - return; - } - if (angularCliPkgJson['version'] === '0.0.0') { - // Internal testing version - return; - } - const cliMajor = new semver_1.SemVer(angularCliPkgJson['version']).major; - // e.g. CLI 8.0 supports '>=8.0.0 <9.0.0', including pre-releases (betas, rcs, snapshots) - // of both 8 and 9. Also allow version "0.0.0" for integration testing in the angular/angular - // repository with the generated development @angular/core npm package which is versioned "0.0.0". - const supportedAngularSemver = `0.0.0 || ^${cliMajor}.0.0-beta || ` + `>=${cliMajor}.0.0 <${cliMajor + 1}.0.0`; - const angularVersion = new semver_1.SemVer(angularPkgJson['version']); - const rxjsVersion = new semver_1.SemVer(rxjsPkgJson['version']); - if (!semver_1.satisfies(angularVersion, supportedAngularSemver, { includePrerelease: true })) { - logger.error(core_1.tags.stripIndents ` - This version of CLI is only compatible with Angular versions ${supportedAngularSemver}, - but Angular version ${angularVersion} was found instead. - - Please visit the link below to find instructions on how to update Angular. - https://update.angular.io/ - ` + '\n'); - process.exit(3); - } - else if (semver_1.gte(angularVersion, '6.0.0-rc.0') && - !semver_1.gte(rxjsVersion, '5.6.0-forward-compat.0') && - !semver_1.gte(rxjsVersion, '6.0.0-beta.0')) { - logger.error(core_1.tags.stripIndents ` - This project uses version ${rxjsVersion} of RxJs, which is not supported by Angular v6+. - The official RxJs version that is supported is 5.6.0-forward-compat.0 and greater. - - Please visit the link below to find instructions on how to update RxJs. - https://docs.google.com/document/d/12nlLt71VLKb-z3YaSGzUfx6mJbc34nsMXtByPUN35cg/edit# - ` + '\n'); - process.exit(3); - } - else if (semver_1.gte(angularVersion, '6.0.0-rc.0') && !semver_1.gte(rxjsVersion, '6.0.0-beta.0')) { - logger.warn(core_1.tags.stripIndents ` - This project uses a temporary compatibility version of RxJs (${rxjsVersion}). - - Please visit the link below to find instructions on how to update RxJs. - https://docs.google.com/document/d/12nlLt71VLKb-z3YaSGzUfx6mJbc34nsMXtByPUN35cg/edit# - ` + '\n'); - } -} -exports.assertCompatibleAngularVersion = assertCompatibleAngularVersion; diff --git a/src/utils/webpack-browser-config.d.ts b/src/utils/webpack-browser-config.d.ts index 1344789eb..20b60b9d7 100644 --- a/src/utils/webpack-browser-config.d.ts +++ b/src/utils/webpack-browser-config.d.ts @@ -1,31 +1,29 @@ -/// /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { BuilderContext } from '@angular-devkit/architect'; -import { logging, virtualFs } from '@angular-devkit/core'; -import * as fs from 'fs'; -import * as webpack from 'webpack'; -import { WebpackConfigOptions } from '../angular-cli-files/models/build-options'; -import { Schema as BrowserBuilderSchema } from '../browser/schema'; +import { Configuration } from 'webpack'; +import { Schema as BrowserBuilderSchema } from '../builders/browser/schema'; import { NormalizedBrowserBuilderSchema } from '../utils'; -import { I18nOptions } from './i18n-options'; -export declare type BrowserWebpackConfigOptions = WebpackConfigOptions; -export declare function generateWebpackConfig(context: BuilderContext, workspaceRoot: string, projectRoot: string, sourceRoot: string | undefined, options: NormalizedBrowserBuilderSchema, webpackPartialGenerator: (wco: BrowserWebpackConfigOptions) => webpack.Configuration[], logger: logging.LoggerApi): Promise; -export declare function generateI18nBrowserWebpackConfigFromContext(options: BrowserBuilderSchema, context: BuilderContext, webpackPartialGenerator: (wco: BrowserWebpackConfigOptions) => webpack.Configuration[], host?: virtualFs.Host): Promise<{ - config: webpack.Configuration; +import { WebpackConfigOptions } from '../utils/build-options'; +import { I18nOptions } from './i18n-webpack'; +export type BrowserWebpackConfigOptions = WebpackConfigOptions; +export type WebpackPartialGenerator = (configurationOptions: BrowserWebpackConfigOptions) => (Promise | Configuration)[]; +export declare function generateWebpackConfig(workspaceRoot: string, projectRoot: string, sourceRoot: string | undefined, projectName: string, options: NormalizedBrowserBuilderSchema, webpackPartialGenerator: WebpackPartialGenerator, logger: BuilderContext['logger'], extraBuildOptions: Partial): Promise; +export declare function generateI18nBrowserWebpackConfigFromContext(options: BrowserBuilderSchema, context: BuilderContext, webpackPartialGenerator: WebpackPartialGenerator, extraBuildOptions?: Partial): Promise<{ + config: Configuration; projectRoot: string; projectSourceRoot?: string; i18n: I18nOptions; }>; -export declare function generateBrowserWebpackConfigFromContext(options: BrowserBuilderSchema, context: BuilderContext, webpackPartialGenerator: (wco: BrowserWebpackConfigOptions) => webpack.Configuration[], host?: virtualFs.Host): Promise<{ - config: webpack.Configuration; +export declare function generateBrowserWebpackConfigFromContext(options: BrowserBuilderSchema, context: BuilderContext, webpackPartialGenerator: WebpackPartialGenerator, extraBuildOptions?: Partial): Promise<{ + config: Configuration; projectRoot: string; projectSourceRoot?: string; }>; -export declare function getIndexOutputFile(options: BrowserBuilderSchema): string; -export declare function getIndexInputFile(options: BrowserBuilderSchema): string; +export declare function getIndexOutputFile(index: BrowserBuilderSchema['index']): string; +export declare function getIndexInputFile(index: BrowserBuilderSchema['index']): string; diff --git a/src/utils/webpack-browser-config.js b/src/utils/webpack-browser-config.js index 018e161f3..4bf5ca2f1 100644 --- a/src/utils/webpack-browser-config.js +++ b/src/utils/webpack-browser-config.js @@ -1,53 +1,65 @@ "use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); Object.defineProperty(exports, "__esModule", { value: true }); -exports.getIndexInputFile = exports.getIndexOutputFile = exports.generateBrowserWebpackConfigFromContext = exports.generateI18nBrowserWebpackConfigFromContext = exports.generateWebpackConfig = void 0; -const core_1 = require("@angular-devkit/core"); -const node_1 = require("@angular-devkit/core/node"); -const path = require("path"); -const webpack_configs_1 = require("../angular-cli-files/models/webpack-configs"); -const read_tsconfig_1 = require("../angular-cli-files/utilities/read-tsconfig"); +exports.generateWebpackConfig = generateWebpackConfig; +exports.generateI18nBrowserWebpackConfigFromContext = generateI18nBrowserWebpackConfigFromContext; +exports.generateBrowserWebpackConfigFromContext = generateBrowserWebpackConfigFromContext; +exports.getIndexOutputFile = getIndexOutputFile; +exports.getIndexInputFile = getIndexInputFile; +const path = __importStar(require("node:path")); +const webpack_1 = require("webpack"); +const webpack_merge_1 = require("webpack-merge"); +const builder_watch_plugin_1 = require("../tools/webpack/plugins/builder-watch-plugin"); const utils_1 = require("../utils"); -const build_browser_features_1 = require("./build-browser-features"); -const environment_options_1 = require("./environment-options"); -const i18n_options_1 = require("./i18n-options"); -const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); -const webpackMerge = require('webpack-merge'); -async function generateWebpackConfig(context, workspaceRoot, projectRoot, sourceRoot, options, webpackPartialGenerator, logger) { +const read_tsconfig_1 = require("../utils/read-tsconfig"); +const i18n_webpack_1 = require("./i18n-webpack"); +async function generateWebpackConfig(workspaceRoot, projectRoot, sourceRoot, projectName, options, webpackPartialGenerator, logger, extraBuildOptions) { // Ensure Build Optimizer is only used with AOT. if (options.buildOptimizer && !options.aot) { throw new Error(`The 'buildOptimizer' option cannot be used without 'aot'.`); } - // Ensure Rollup Concatenation is only used with compatible options. - if (options.experimentalRollupPass) { - if (!options.aot) { - throw new Error(`The 'experimentalRollupPass' option cannot be used without 'aot'.`); - } - if (options.vendorChunk || options.commonChunk || options.namedChunks) { - throw new Error(`The 'experimentalRollupPass' option cannot be used with the` - + `'vendorChunk', 'commonChunk', 'namedChunks' options set to true.`); - } - } const tsConfigPath = path.resolve(workspaceRoot, options.tsConfig); - const tsConfig = read_tsconfig_1.readTsconfig(tsConfigPath); - // tslint:disable-next-line:no-implicit-dependencies - const ts = await Promise.resolve().then(() => require('typescript')); - // At the moment, only the browser builder supports differential loading - // However this config generation is used by multiple builders such as dev-server - const scriptTarget = tsConfig.options.target || ts.ScriptTarget.ES5; - const buildBrowserFeatures = new build_browser_features_1.BuildBrowserFeatures(projectRoot, scriptTarget); - const differentialLoading = context.builder.builderName === 'browser' && - !options.watch && - buildBrowserFeatures.isDifferentialLoadingNeeded(); - let buildOptions = { ...options }; - if (differentialLoading) { - buildOptions = { - ...options, - // Under downlevel differential loading we copy the assets outside of webpack. - assets: [], - esVersionInFileName: true, - }; - } - const supportES2015 = scriptTarget !== ts.ScriptTarget.JSON && scriptTarget > ts.ScriptTarget.ES5; + const tsConfig = await (0, read_tsconfig_1.readTsconfig)(tsConfigPath); + const buildOptions = { ...options, ...extraBuildOptions }; const wco = { root: workspaceRoot, logger: logger.createChild('webpackConfigOptions'), @@ -56,35 +68,18 @@ async function generateWebpackConfig(context, workspaceRoot, projectRoot, source buildOptions, tsConfig, tsConfigPath, - supportES2015, - differentialLoadingMode: differentialLoading, + projectName, }; - wco.buildOptions.progress = utils_1.defaultProgress(wco.buildOptions.progress); - const partials = webpackPartialGenerator(wco); - const webpackConfig = webpackMerge(partials); - if (supportES2015) { - if (!webpackConfig.resolve) { - webpackConfig.resolve = {}; - } - if (!webpackConfig.resolve.alias) { - webpackConfig.resolve.alias = {}; - } - webpackConfig.resolve.alias['zone.js/dist/zone'] = 'zone.js/dist/zone-evergreen'; - } - if (environment_options_1.profilingEnabled) { - const esVersionInFileName = webpack_configs_1.getEsVersionForFileName(tsConfig.options.target, wco.buildOptions.esVersionInFileName); - const smp = new SpeedMeasurePlugin({ - outputFormat: 'json', - outputTarget: path.resolve(workspaceRoot, `speed-measure-plugin${esVersionInFileName}.json`), - }); - return smp.wrap(webpackConfig); - } + wco.buildOptions.progress = (0, utils_1.defaultProgress)(wco.buildOptions.progress); + const partials = await Promise.all(webpackPartialGenerator(wco)); + const webpackConfig = (0, webpack_merge_1.merge)(partials); return webpackConfig; } -exports.generateWebpackConfig = generateWebpackConfig; -async function generateI18nBrowserWebpackConfigFromContext(options, context, webpackPartialGenerator, host = new node_1.NodeJsSyncHost()) { - const { buildOptions, i18n } = await i18n_options_1.configureI18nBuild(context, options); - const result = await generateBrowserWebpackConfigFromContext(buildOptions, context, webpackPartialGenerator, host); +async function generateI18nBrowserWebpackConfigFromContext(options, context, webpackPartialGenerator, extraBuildOptions = {}) { + const { buildOptions, i18n } = await (0, i18n_webpack_1.configureI18nBuild)(context, options); + const result = await generateBrowserWebpackConfigFromContext(buildOptions, context, (wco) => { + return webpackPartialGenerator(wco); + }, extraBuildOptions); const config = result.config; if (i18n.shouldInline) { // Remove localize "polyfill" if in AOT mode @@ -92,27 +87,26 @@ async function generateI18nBrowserWebpackConfigFromContext(options, context, web if (!config.resolve) { config.resolve = {}; } - if (!config.resolve.alias) { - config.resolve.alias = {}; + if (Array.isArray(config.resolve.alias)) { + config.resolve.alias.push({ + name: '@angular/localize/init', + alias: false, + }); + } + else { + if (!config.resolve.alias) { + config.resolve.alias = {}; + } + config.resolve.alias['@angular/localize/init'] = false; } - config.resolve.alias['@angular/localize/init'] = require.resolve('./empty.js'); } // Update file hashes to include translation file content - const i18nHash = Object.values(i18n.locales).reduce((data, locale) => data + (locale.integrity || ''), ''); - if (!config.plugins) { - config.plugins = []; - } + const i18nHash = Object.values(i18n.locales).reduce((data, locale) => data + locale.files.map((file) => file.integrity || '').join('|'), ''); + config.plugins ??= []; config.plugins.push({ apply(compiler) { - compiler.hooks.compilation.tap('build-angular', compilation => { - // Webpack typings do not contain template hashForChunk hook - // tslint:disable-next-line: no-any - compilation.mainTemplate.hooks.hashForChunk.tap('build-angular', (hash) => { - hash.update('$localize' + i18nHash); - }); - // Webpack typings do not contain hooks property - // tslint:disable-next-line: no-any - compilation.chunkTemplate.hooks.hashForChunk.tap('build-angular', (hash) => { + compiler.hooks.compilation.tap('build-angular', (compilation) => { + webpack_1.javascript.JavascriptModulesPlugin.getCompilationHooks(compilation).chunkHash.tap('build-angular', (_, hash) => { hash.update('$localize' + i18nHash); }); }); @@ -121,43 +115,46 @@ async function generateI18nBrowserWebpackConfigFromContext(options, context, web } return { ...result, i18n }; } -exports.generateI18nBrowserWebpackConfigFromContext = generateI18nBrowserWebpackConfigFromContext; -async function generateBrowserWebpackConfigFromContext(options, context, webpackPartialGenerator, host = new node_1.NodeJsSyncHost()) { +async function generateBrowserWebpackConfigFromContext(options, context, webpackPartialGenerator, extraBuildOptions = {}) { const projectName = context.target && context.target.project; if (!projectName) { throw new Error('The builder requires a target.'); } - const workspaceRoot = core_1.normalize(context.workspaceRoot); + const workspaceRoot = context.workspaceRoot; const projectMetadata = await context.getProjectMetadata(projectName); - const projectRoot = core_1.resolve(workspaceRoot, core_1.normalize(projectMetadata.root || '')); - const projectSourceRoot = projectMetadata.sourceRoot; - const sourceRoot = projectSourceRoot - ? core_1.resolve(workspaceRoot, core_1.normalize(projectSourceRoot)) - : undefined; - const normalizedOptions = utils_1.normalizeBrowserSchema(host, workspaceRoot, projectRoot, sourceRoot, options); - const config = await generateWebpackConfig(context, core_1.getSystemPath(workspaceRoot), core_1.getSystemPath(projectRoot), sourceRoot && core_1.getSystemPath(sourceRoot), normalizedOptions, webpackPartialGenerator, context.logger); + const projectRoot = path.join(workspaceRoot, projectMetadata.root ?? ''); + const sourceRoot = projectMetadata.sourceRoot; + const projectSourceRoot = sourceRoot ? path.join(workspaceRoot, sourceRoot) : undefined; + const normalizedOptions = (0, utils_1.normalizeBrowserSchema)(workspaceRoot, projectRoot, projectSourceRoot, options, projectMetadata, context.logger); + const config = await generateWebpackConfig(workspaceRoot, projectRoot, projectSourceRoot, projectName, normalizedOptions, webpackPartialGenerator, context.logger, extraBuildOptions); + // If builder watch support is present in the context, add watch plugin + // This is internal only and currently only used for testing + const watcherFactory = context.watcherFactory; + if (watcherFactory) { + if (!config.plugins) { + config.plugins = []; + } + config.plugins.push(new builder_watch_plugin_1.BuilderWatchPlugin(watcherFactory)); + } return { config, - projectRoot: core_1.getSystemPath(projectRoot), - projectSourceRoot: sourceRoot && core_1.getSystemPath(sourceRoot), + projectRoot, + projectSourceRoot, }; } -exports.generateBrowserWebpackConfigFromContext = generateBrowserWebpackConfigFromContext; -function getIndexOutputFile(options) { - if (typeof options.index === 'string') { - return path.basename(options.index); +function getIndexOutputFile(index) { + if (typeof index === 'string') { + return path.basename(index); } else { - return options.index.output || 'index.html'; + return index.output || 'index.html'; } } -exports.getIndexOutputFile = getIndexOutputFile; -function getIndexInputFile(options) { - if (typeof options.index === 'string') { - return options.index; +function getIndexInputFile(index) { + if (typeof index === 'string') { + return index; } else { - return options.index.input; + return index.input; } } -exports.getIndexInputFile = getIndexInputFile; diff --git a/src/utils/webpack-diagnostics.d.ts b/src/utils/webpack-diagnostics.d.ts new file mode 100644 index 000000000..571b5dd1c --- /dev/null +++ b/src/utils/webpack-diagnostics.d.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import type { Compilation } from 'webpack'; +export declare function addWarning(compilation: Compilation, message: string): void; +export declare function addError(compilation: Compilation, message: string): void; diff --git a/src/utils/webpack-diagnostics.js b/src/utils/webpack-diagnostics.js new file mode 100644 index 000000000..2d3f23943 --- /dev/null +++ b/src/utils/webpack-diagnostics.js @@ -0,0 +1,17 @@ +"use strict"; +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addWarning = addWarning; +exports.addError = addError; +function addWarning(compilation, message) { + compilation.warnings.push(new compilation.compiler.webpack.WebpackError(message)); +} +function addError(compilation, message) { + compilation.errors.push(new compilation.compiler.webpack.WebpackError(message)); +} diff --git a/src/utils/workers.d.ts b/src/utils/workers.d.ts deleted file mode 100644 index 4f51e7afd..000000000 --- a/src/utils/workers.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -/** - * Use CPU count -1 with limit to 7 for workers not to clog the system. - * Some environments, like CircleCI which use Docker report a number of CPUs by the host and not the count of available. - * This cause `Error: Call retries were exceeded` errors when trying to use them. - * - * See: - * - * https://github.com/nodejs/node/issues/28762 - * - * https://github.com/webpack-contrib/terser-webpack-plugin/issues/143 - * - * https://github.com/angular/angular-cli/issues/16860#issuecomment-588828079 - * - */ -export declare const maxWorkers: number; diff --git a/src/utils/workers.js b/src/utils/workers.js deleted file mode 100644 index 4c3463ef1..000000000 --- a/src/utils/workers.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.maxWorkers = void 0; -const os_1 = require("os"); -/** - * Use CPU count -1 with limit to 7 for workers not to clog the system. - * Some environments, like CircleCI which use Docker report a number of CPUs by the host and not the count of available. - * This cause `Error: Call retries were exceeded` errors when trying to use them. - * - * See: - * - * https://github.com/nodejs/node/issues/28762 - * - * https://github.com/webpack-contrib/terser-webpack-plugin/issues/143 - * - * https://github.com/angular/angular-cli/issues/16860#issuecomment-588828079 - * - */ -exports.maxWorkers = Math.max(Math.min(os_1.cpus().length, 8) - 1, 1); diff --git a/uniqueId b/uniqueId index 5c29e9443..56fe91ce3 100644 --- a/uniqueId +++ b/uniqueId @@ -1 +1 @@ -Wed Jun 10 2020 09:51:08 GMT+0000 (Coordinated Universal Time) \ No newline at end of file +Tue Oct 07 2025 09:54:10 GMT+0000 (Coordinated Universal Time) \ No newline at end of file