diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index d44ea9ee88c5..000000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,26 +0,0 @@ -environment: - # Note: if updating to Node 10, use at least 10.5.0 to include a fix for - # https://github.com/nodejs/node/issues/20297 - nodejs_version: "8.9.2" # Same version as used in CircleCI. - -matrix: - fast_finish: true - -install: - - ps: Install-Product node $env:nodejs_version - - npm install - # Appveyor (via chocolatey) cannot use older versions of Chrome: - # https://github.com/chocolatey/chocolatey-coreteampackages/tree/master/automatic/googlechrome - - npm run webdriver-update-appveyor - -test_script: - - node --version - - npm --version - - npm test - - npm run test-large - -build: off -deploy: off - -cache: - - node_modules -> package-lock.json diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 000000000000..52ebdac6af4a --- /dev/null +++ b/.bazelignore @@ -0,0 +1,20 @@ +.git +dist +node_modules +packages/angular/cli/node_modules +packages/angular/create/node_modules +packages/angular/pwa/node_modules +packages/angular/build/node_modules +packages/angular/ssr/node_modules +packages/angular_devkit/architect/node_modules +packages/angular_devkit/architect_cli/node_modules +packages/angular_devkit/build_angular/node_modules +packages/angular_devkit/build_webpack/node_modules +packages/angular_devkit/core/node_modules +packages/angular_devkit/schematics/node_modules +packages/angular_devkit/schematics_cli/node_modules +packages/ngtools/webpack/node_modules +packages/schematics/angular/node_modules +modules/testing/builder/node_modules +tests/node_modules +tools/baseline_browserslist/node_modules diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 000000000000..4f79c86cf3b4 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,178 @@ +# Disable NG CLI TTY mode +build --action_env=NG_FORCE_TTY=false + +# Required by `rules_ts`. +common --@aspect_rules_ts//ts:skipLibCheck=always +common --@aspect_rules_ts//ts:default_to_tsc_transpiler + +# Make TypeScript compilation fast, by keeping a few copies of the compiler +# running as daemons, and cache SourceFile AST's to reduce parse time. +build --strategy=TypeScriptCompile=worker + +# Enable debugging tests with --config=debug +test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results + +# Enable debugging tests with --config=no-sharding +# The below is useful to while using `fit` and `fdescribe` to avoid sharing and re-runs of failed flaky tests. +test:no-sharding --flaky_test_attempts=1 --test_sharding_strategy=disabled + +############################### +# Filesystem interactions # +############################### + +# Create symlinks in the project: +# - dist/bin for outputs +# - dist/testlogs, dist/genfiles +# - bazel-out +# NB: bazel-out should be excluded from the editor configuration. +# The checked-in /.vscode/settings.json does this for VSCode. +# Other editors may require manual config to ignore this directory. +# In the past, we saw a problem where VSCode traversed a massive tree, opening file handles and +# eventually a surprising failure with auto-discovery of the C++ toolchain in +# MacOS High Sierra. +# See https://github.com/bazelbuild/bazel/issues/4603 +build --symlink_prefix=dist/ + +# Turn off legacy external runfiles +build --nolegacy_external_runfiles + +# Turn on --incompatible_strict_action_env which was on by default +# in Bazel 0.21.0 but turned off again in 0.22.0. Follow +# https://github.com/bazelbuild/bazel/issues/7026 for more details. +# This flag is needed to so that the bazel cache is not invalidated +# when running bazel via `pnpm bazel`. +# See https://github.com/angular/angular/issues/27514. +build --incompatible_strict_action_env +run --incompatible_strict_action_env +test --incompatible_strict_action_env + +# Enable remote caching of build/action tree +build --experimental_remote_merkle_tree_cache + +# Ensure that tags applied in BUILDs propagate to actions +common --experimental_allow_tags_propagation + +# Ensure sandboxing is enabled even for exclusive tests +test --incompatible_exclusive_test_sandboxed + +############################### +# Saucelabs support # +# Turn on these settings with # +# --config=saucelabs # +############################### + +# Expose SauceLabs environment to actions +# These environment variables are needed by +# web_test_karma to run on Saucelabs +test:saucelabs --action_env=SAUCE_USERNAME +test:saucelabs --action_env=SAUCE_ACCESS_KEY +test:saucelabs --action_env=SAUCE_READY_FILE +test:saucelabs --action_env=SAUCE_PID_FILE +test:saucelabs --action_env=SAUCE_TUNNEL_IDENTIFIER +test:saucelabs --define=KARMA_WEB_TEST_MODE=SL_REQUIRED + +############################### +# Release support # +# Turn on these settings with # +# --config=release # +############################### + +# Releases should always be stamped with version control info +# This command assumes node on the path and is a workaround for +# https://github.com/bazelbuild/bazel/issues/4802 +build:release --workspace_status_command="pnpm -s ng-dev release build-env-stamp --mode=release" +build:release --stamp + +build:snapshot --workspace_status_command="pnpm -s ng-dev release build-env-stamp --mode=snapshot" +build:snapshot --stamp +build:snapshot --//:enable_snapshot_repo_deps + +build:e2e --workspace_status_command="pnpm -s ng-dev release build-env-stamp --mode=release" +build:e2e --stamp +test:e2e --test_timeout=3600 --experimental_ui_max_stdouterr_bytes=2097152 + +# Retry in the event of flakes +test:e2e --flaky_test_attempts=2 + +build:local --//:enable_package_json_tar_deps + +############################### +# Output # +############################### + +# A more useful default output mode for bazel query +# Prints eg. "ng_module rule //foo:bar" rather than just "//foo:bar" +query --output=label_kind + +# By default, failing tests don't print any output, it goes to the log file +test --test_output=errors +################################ +# Remote Execution Setup # +################################ + +# Use the Angular team internal GCP instance for remote execution. +build:remote --remote_instance_name=projects/internal-200822/instances/primary_instance +build:remote --bes_instance_name=internal-200822 + +# Starting with Bazel 0.27.0 strategies do not need to be explicitly +# defined. See https://github.com/bazelbuild/bazel/issues/7480 +build:remote --define=EXECUTOR=remote + +# Setup the remote build execution servers. +build:remote --remote_cache=remotebuildexecution.googleapis.com +build:remote --remote_executor=remotebuildexecution.googleapis.com +build:remote --remote_timeout=600 +build:remote --jobs=150 + +# Setup the toolchain and platform for the remote build execution. The platform +# is provided by the shared dev-infra package and targets k8 remote containers. +build:remote --extra_execution_platforms=@devinfra//bazel/remote-execution:platform_with_network +build:remote --host_platform=@devinfra//bazel/remote-execution:platform_with_network +build:remote --platforms=@devinfra//bazel/remote-execution:platform_with_network + +# Set remote caching settings +build:remote --remote_accept_cached=true +build:remote --remote_upload_local_results=false + +# Force remote executions to consider the entire run as linux. +# This is required for OSX cross-platform RBE. +build:remote --cpu=k8 +build:remote --host_cpu=k8 + +# Set up authentication mechanism for RBE +build:remote --google_default_credentials + +# Use HTTP remote cache +build:remote-cache --remote_cache=https://storage.googleapis.com/angular-team-cache +build:remote-cache --remote_accept_cached=true +build:remote-cache --remote_upload_local_results=false +build:remote-cache --google_default_credentials + +# Additional flags added when running a "trusted build" with additional access +build:trusted-build --remote_upload_local_results=true + +# Fixes issues with browser archives and files with spaces. Could be +# removed in Bazel 8 when Bazel runfiles supports spaces. +build --experimental_inprocess_symlink_creation + +#################################################### +# rules_js specific flags +#################################################### + +# TODO(josephperrott): investigate if this can be removed eventually. +# Prevents the npm package extract from occuring or caching on RBE which overwhelms our quota +build --modify_execution_info=NpmPackageExtract=+no-remote + +# Allow the Bazel server to check directory sources for changes. `rules_js` previously +# heavily relied on this, but still uses directory "inputs" in some cases. +# See: https://github.com/aspect-build/rules_js/issues/1408. +startup --host_jvm_args=-DBAZEL_TRACK_SOURCE_DIRECTORIES=1 + +#################################################### +# User bazel configuration +# NOTE: This needs to be the *last* entry in the config. +#################################################### + +# Load any settings which are specific to the current user. Needs to be *last* statement +# in this config, as the user configuration should be able to overwrite flags from this file. +try-import .bazelrc.user diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 000000000000..93c8ddab9fef --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +7.6.0 diff --git a/.circleci/bazel.rc b/.circleci/bazel.rc deleted file mode 100644 index 0cf9444d5d72..000000000000 --- a/.circleci/bazel.rc +++ /dev/null @@ -1,20 +0,0 @@ -# These options are enabled when running on CI -# We do this by copying this file to /etc/bazel.bazelrc at the start of the build. - -# Echo all the configuration settings and their source -build --announce_rc - -# Don't be spammy in the logs -build --noshow_progress - -# Don't run manual tests -test --test_tag_filters=-manual - -# Workaround https://github.com/bazelbuild/bazel/issues/3645 -# Bazel doesn't calculate the memory ceiling correctly when running under Docker. -# Limit Bazel to consuming resources that fit in CircleCI "medium" class which is the default: -# https://circleci.com/docs/2.0/configuration-reference/#resource_class -build --local_resources=3072,2.0,1.0 - -# Retry in the event of flakes -test --flaky_test_attempts=2 diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index b01bececf435..000000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,157 +0,0 @@ -version: 2 - -_defaults: &defaults - working_directory: ~/ng - docker: - - image: angular/ngcontainer:0.3.0 - -_post_checkout: &post_checkout - post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge" - -_root_package_lock_key: &_root_package_lock_key - key: angular_devkit-{{ checksum "package-lock.json" }}-0.3.0 - -jobs: - install: - <<: *defaults - steps: - - checkout: *post_checkout - - restore_cache: *_root_package_lock_key - - run: npm install --no-save - - save_cache: - <<: *_root_package_lock_key - paths: - - "node_modules" - - lint: - <<: *defaults - steps: - - checkout: *post_checkout - - restore_cache: *_root_package_lock_key - - run: npm run lint - - validate: - <<: *defaults - steps: - - checkout: *post_checkout - - restore_cache: *_root_package_lock_key - - run: npm run validate -- --ci - - test: - <<: *defaults - steps: - - checkout: *post_checkout - - restore_cache: *_root_package_lock_key - - run: npm run test -- --code-coverage --full - - test-large: - <<: *defaults - resource_class: large - parallelism: 4 - steps: - - checkout: *post_checkout - - restore_cache: *_root_package_lock_key - - run: npm run webdriver-update-circleci - - run: npm run test-large -- --code-coverage --full --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} - - e2e-cli: - <<: *defaults - environment: - BASH_ENV: ~/.profile - resource_class: xlarge - parallelism: 4 - steps: - - checkout: *post_checkout - - restore_cache: *_root_package_lock_key - - run: xvfb-run -a npm run test-cli-e2e -- --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} - - build: - <<: *defaults - steps: - - checkout: *post_checkout - - restore_cache: *_root_package_lock_key - - run: npm run admin -- build - - build-bazel: - <<: *defaults - resource_class: large - steps: - - checkout: *post_checkout - - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - - run: bazel run @nodejs//:npm install - - run: bazel build //packages/... - - snapshot_publish: - <<: *defaults - steps: - - checkout: *post_checkout - - restore_cache: *_root_package_lock_key - - run: - name: Decrypt Credentials - command: | - openssl aes-256-cbc -d -in .circleci/github_token -k "${KEY}" -out ~/github_token - - run: - name: Deployment to Snapshot - command: | - npm run admin -- snapshots --verbose --githubTokenFile=${HOME}/github_token - - publish: - <<: *defaults - steps: - - checkout: *post_checkout - - restore_cache: *_root_package_lock_key - - run: - name: Decrypt Credentials - command: | - openssl aes-256-cbc -d -in .circleci/npm_token -k "${KEY}" -out ~/.npmrc - - run: - name: Deployment to NPM - command: | - npm run admin -- publish --verbose - -workflows: - version: 2 - default_workflow: - jobs: - - install - - lint: - requires: - - install - - validate: - requires: - - install - - build: - requires: - - lint - - validate - - build-bazel: - requires: - - lint - - validate - - test: - requires: - - build - - test-large: - requires: - - build - - e2e-cli: - requires: - - build - - snapshot_publish: - requires: - - test - - build - - e2e-cli - filters: - branches: - only: master - - publish: - requires: - - test - - build - - snapshot_publish - filters: - tags: - only: /^v\d+/ - branches: - ignore: /.*/ diff --git a/.circleci/github_token b/.circleci/github_token deleted file mode 100644 index 450cb2c93f8c..000000000000 --- a/.circleci/github_token +++ /dev/null @@ -1 +0,0 @@ -Salted__z�����"B��Y��|�ۍ�V�QֳUzW�/G��R �e}j�% ���<%������ \ No newline at end of file diff --git a/.circleci/npm_token b/.circleci/npm_token deleted file mode 100644 index 5a1bb6303052..000000000000 --- a/.circleci/npm_token +++ /dev/null @@ -1 +0,0 @@ -Salted__�/��L ���ö���;��(.|��� ��C��Ԓ����5`h�8��i8J�o*�?}���3�0f�!�'�B�̠�"UƊ&K!�%�ɵڤ \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 36e19acf952e..c0a70d2acb14 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# http://editorconfig.org +# https://editorconfig.org root = true @@ -9,6 +9,7 @@ indent_size = 2 insert_final_newline = true spaces_around_brackets = inside trim_trailing_whitespace = true +quote_type = single [*.md] insert_final_newline = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..de32b85d6693 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# JS and TS files must always use LF for tools to work +*.js eol=lf +*.ts eol=lf +*.json eol=lf +*.css eol=lf +*.scss eol=lf +*.less eol=lf +*.html eol=lf +*.svg eol=lf diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index ee7b3ffbf4ef..000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,43 +0,0 @@ - -### Bug Report or Feature Request (mark with an `x`) -``` -- [ ] bug report -> please search issues before submitting -- [ ] feature request -``` - -### Area -``` -- [ ] devkit -- [ ] schematics -``` - -### Versions - - - -### Repro steps - - - -### The log given by the failure - - - -### Desired functionality - - - -### Mention any other details that might be useful - diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml new file mode 100644 index 000000000000..5c4ea7d2cbdb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -0,0 +1,102 @@ +name: Bug report +description: Report a bug in Angular CLI +body: + - type: markdown + attributes: + value: | + Oh hi there! + + To expedite issue processing please search open and closed issues before submitting a new one. + Existing issues often contain information about workarounds, resolution, or progress updates. + - type: dropdown + id: command + attributes: + label: Command + description: Can you pin-point the command or commands that are effected by this bug? + options: + - add + - build + - config + - doc + - e2e + - extract-i18n + - generate + - help + - lint + - new + - other + - run + - serve + - test + - update + - version + multiple: true + validations: + required: true + - type: checkboxes + id: is-regression + attributes: + label: Is this a regression? + description: Did this behavior use to work in the previous version? + options: + - label: Yes, this behavior used to work in the previous version + - type: input + id: version-bug-was-not-present + attributes: + label: The previous version in which this bug was not present was + validations: + required: false + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the problem. + validations: + required: true + - type: textarea + id: minimal-reproduction + attributes: + label: Minimal Reproduction + description: | + Simple steps to reproduce this bug. + + **Please include:** + * commands run (including args) + * packages added + * related code changes + + + If reproduction steps are not enough for reproduction of your issue, please create a minimal GitHub repository with the reproduction of the issue. + A good way to make a minimal reproduction is to create a new app via `ng new repro-app` and add the minimum possible code to show the problem. + Share the link to the repo below along with step-by-step instructions to reproduce the problem, as well as expected and actual behavior. + + Issues that don't have enough info and can't be reproduced will be closed. + + You can read more about issue submission guidelines [here](https://github.com/angular/angular-cli/blob/main/CONTRIBUTING.md#-submitting-an-issue). + validations: + required: true + - type: textarea + id: exception-or-error + attributes: + label: Exception or Error + description: If the issue is accompanied by an exception or an error, please share it below. + render: text + validations: + required: false + - type: textarea + id: environment + attributes: + label: Your Environment + description: Run `ng version` and paste output below. + render: text + validations: + required: true + - type: textarea + id: other + attributes: + label: Anything else relevant? + description: | + Is this a browser specific issue? If so, please specify the browser and version. + Do any of these matter: operating system, IDE, package manager, HTTP server, ...? If so, please mention it below. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.yml b/.github/ISSUE_TEMPLATE/2-feature-request.yml new file mode 100644 index 000000000000..4a01698e0f37 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-feature-request.yml @@ -0,0 +1,55 @@ +name: Feature request +description: Suggest a feature for Angular CLI +body: + - type: markdown + attributes: + value: | + Oh hi there! + + To expedite issue processing please search open and closed issues before submitting a new one. + Existing issues often contain information about workarounds, resolution, or progress updates. + - type: dropdown + id: command + attributes: + label: Command + description: Can you pin-point the command or commands that are relevant for this feature request? + options: + - add + - build + - config + - doc + - e2e + - extract-i18n + - generate + - help + - lint + - new + - run + - serve + - test + - update + - version + multiple: true + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the problem or missing capability. + validations: + required: true + - type: textarea + id: desired-solution + attributes: + label: Describe the solution you'd like + description: If you have a solution in mind, please describe it. + validations: + required: false + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: Have you considered any alternative solutions or workarounds? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..898698af3906 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,17 @@ +blank_issues_enabled: false +contact_links: + - name: Docs or angular.dev issue report + url: https://github.com/angular/angular/issues/new + about: Report an issue in Angular's documentation or angular.dev application + - name: Security issue disclosure + url: https://angular.dev/best-practices/security + about: Report a security issue in Angular Framework, Material, or CLI + - name: Support request + url: https://github.com/angular/angular-cli/blob/main/CONTRIBUTING.md#question + about: Questions and requests for support. + - name: Angular Framework + url: https://github.com/angular/angular/issues/new/choose + about: Issues and feature requests for Angular Framework + - name: Angular Material + url: https://github.com/angular/components/issues/new/choose + about: Issues and feature requests for Angular Material diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..3214dde0a4f4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,43 @@ +## PR Checklist + +Please check to confirm your PR fulfills the following requirements: + + + +- [ ] The commit message follows our guidelines: https://github.com/angular/angular-cli/blob/main/CONTRIBUTING.md#-commit-message-guidelines +- [ ] Tests for the changes have been added (for bug fixes / features) +- [ ] Docs have been added / updated (for bug fixes / features) + +## PR Type + +What kind of change does this PR introduce? + + + +- [ ] Bugfix +- [ ] Feature +- [ ] Code style update (formatting, local variables) +- [ ] Refactoring (no functional changes, no api changes) +- [ ] Build related changes +- [ ] CI related changes +- [ ] Documentation content changes +- [ ] Other... Please describe: + +## What is the current behavior? + + + +Issue Number: N/A + +## What is the new behavior? + + + +## Does this PR introduce a breaking change? + +- [ ] Yes +- [ ] No + + + +## Other information diff --git a/.github/SAVED_REPLIES.md b/.github/SAVED_REPLIES.md new file mode 100644 index 000000000000..1237bc279e11 --- /dev/null +++ b/.github/SAVED_REPLIES.md @@ -0,0 +1,101 @@ +# Saved Responses for Angular CLI's Issue Tracker + +The following are canned responses that the Angular CLI team should use to close issues on our issue tracker that fall into the listed resolution categories. + +Since GitHub currently doesn't allow us to have a repository-wide or organization-wide list of [saved replies](https://help.github.com/articles/working-with-saved-replies/), these replies need to be maintained by individual team members. Since the responses can be modified in the future, all responses are versioned to simplify the process of keeping the responses up to date. + +## Angular CLI: Already Fixed (v1) + +``` +Thanks for reporting this issue. Luckily, it has already been fixed in one of the recent releases. Please update to the most recent version to resolve the problem. + +If the problem persists in your application after upgrading, please open a new issue, provide a simple repository reproducing the problem, and describe the difference between the expected and current behavior. You can use `ng new repro-app` to create a new project where you reproduce the problem. +``` + +## Angular CLI: Don't Understand (v1) + +``` +I'm sorry, but we don't understand the problem you are reporting. + +If the problem persists, please open a new issue, provide a simple repository reproducing the problem, and describe the difference between the expected and current behavior. You can use `ng new repro-app` to create a new project where you reproduce the problem. +``` + +## Angular CLI: Duplicate (v1.1) + +``` +Thanks for reporting this issue. However, this issue is a duplicate of #. Please subscribe to that issue for future updates. +``` + +## Angular CLI: Insufficient Information Provided (v1) + +``` +Thanks for reporting this issue. However, you didn't provide sufficient information for us to understand and reproduce the problem. Please check out [our submission guidelines](https://github.com/angular/angular-cli/blob/main/CONTRIBUTING.md#-submitting-an-issue) to understand why we can't act on issues that are lacking important information. + +If the problem persists, please file a new issue and ensure you provide all of the required information when filling out the issue template. +``` + +## Angular CLI: NPM install issue (v1) + +``` +This seems like a problem with your node/npm and not with Angular CLI. + +Please have a look at the [fixing npm permissions page](https://docs.npmjs.com/getting-started/fixing-npm-permissions), [common errors page](https://docs.npmjs.com/troubleshooting/common-errors), [npm issue tracker](https://github.com/npm/npm/issues), or open a new issue if the problem you are experiencing isn't known. +``` + +## Angular CLI: Issue Outside of Angular CLI (v1.1) + +``` +I'm sorry, but this issue is not caused by Angular CLI. Please contact the author(s) of the project or file an issue on their issue tracker. +``` + +## Angular CLI: Non-reproducible (v1) + +``` +I'm sorry, but we can't reproduce the problem following the instructions you provided. +Remember that we have a large number of issues to resolve, and have only a limited amount of time to reproduce your issue. +Short, explicit instructions make it much more likely we'll be able to reproduce the problem so we can fix it. + +If the problem persists, please open a new issue following [our submission guidelines](https://github.com/angular/angular-cli/blob/main/CONTRIBUTING.md#-submitting-an-issue). + +A good way to make a minimal repro is to create a new app via `ng new repro-app` and adding the minimum possible code to show the problem. Then you can push this repository to github and link it here. +``` + +## Angular CLI: Obsolete (v1) + +``` +Thanks for reporting this issue. This issue is now obsolete due to changes in the recent releases. Please update to the most recent Angular CLI version. + +If the problem persists after upgrading, please open a new issue, provide a simple repository reproducing the problem, and describe the difference between the expected and current behavior. +``` + +## Angular CLI: Support Request (v1) + +``` +Hello, we reviewed this issue and determined that it doesn't fall into the bug report or feature request category. This issue tracker is not suitable for support requests, please repost your issue on [StackOverflow](https://stackoverflow.com/) using tag `angular-cli`. + +If you are wondering why we don't resolve support issues via the issue tracker, please [check out this explanation](https://github.com/angular/angular-cli/blob/main/CONTRIBUTING.md#-got-a-question-or-problem). +``` + +## Angular CLI: Static Analysis errors (v1) + +``` +Hello, errors like `Error encountered resolving symbol values statically` mean that there has been some problem in statically analyzing your app. + +Angular CLI always runs *some* static analysis, even in JIT mode, in order to discover lazy-loaded routes. +This may cause a lot of static analysis errors to surface when importing your project into the CLI, or upgrading for older versions where we didn't run this kind of analysis. + +Below are good resources on how to debug these errors: +- https://gist.github.com/chuckjaz/65dcc2fd5f4f5463e492ed0cb93bca60 +- https://github.com/rangle/angular-2-aot-sandbox#aot-dos-and-donts + +If your problem still persists, it might be a bug with the Angular Compiler itself. +In that case, please open an issue in https://github.com/angular/angular. +``` + +## Angular CLI: Lockfiles (v1) + +``` +I'd like to remind everyone that **you only have reproducible installs if you use a lockfile**. Both [NPM v5+](https://docs.npmjs.com/files/package-locks) and [Yarn](https://yarnpkg.com/lang/en/docs/yarn-lock/) support lockfiles. If your CI works one day but not the next and you did not change your code or `package.json`, it is likely because one of your dependencies had a bad release and you did not have a lockfile. + +**It is your responsibility as a library consumer to use lockfiles**. No one wants to do a release with bugs but it sometimes happens, and the best we can do is to fix it as fast as possible with a new release. When you have a couple of thousand total dependencies it is only a matter of time until one of them has a bad release. +``` diff --git a/.github/angular-robot.yml b/.github/angular-robot.yml new file mode 100644 index 000000000000..3e3a56a25603 --- /dev/null +++ b/.github/angular-robot.yml @@ -0,0 +1,92 @@ +# Configuration for angular-robot + +# options for the merge plugin +merge: + # the status will be added to your pull requests + status: + # set to true to disable + disabled: true + # the name of the status + context: 'ci/angular: merge status' + # text to show when all checks pass + successText: 'All checks passed!' + # text to show when some checks are failing + failureText: 'The following checks are failing:' + + # comment that will be added to a PR when there is a conflict, leave empty or set to false to disable + mergeConflictComment: >- + Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges. + + Please help to unblock it by resolving these conflicts. Thanks! + + # label to monitor + mergeLabel: 'action: merge' + + # list of checks that will determine if the merge label can be added + checks: + # whether the PR shouldn't have a conflict with the base branch + noConflict: true + # whether the PR should have all reviews completed. + requireReviews: true + # list of labels that a PR needs to have, checked with a regexp (e.g. "target:" will work for the label "target: major") + requiredLabels: + - 'target: *' + + # list of labels that a PR shouldn't have, checked after the required labels with a regexp + forbiddenLabels: + - 'action: cleanup' + - 'action: review' + - 'PR state: blocked' + + # list of PR statuses that need to be successful + # NOTE: Required PR statuses are now exclusively handled via Github configuration + requiredStatuses: [] + + # the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable + # {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option + # {{PLACEHOLDER}} will be replaced by the list of failing checks + mergeRemovedComment: >- + I see that you just added the `{{MERGE_LABEL}}` label, but the following + checks are still failing: + + {{PLACEHOLDER}} + + **If you want your PR to be merged, it has to pass all the CI checks.** + + If you can't get the PR to a green state due to flakes or broken `main`, + please try rebasing to `main` and/or restarting the CI job. If that fails + and you believe that the issue is not due to your change, please contact the + caretaker and ask for help. +# options for the triage plugin +triage: + # set to true to disable + disabled: true + # number of the milestone to apply when the issue has not been triaged yet + needsTriageMilestone: 11, + # number of the milestone to apply when the issue is triaged + defaultMilestone: 12, + # arrays of labels that determine if an issue has been triaged by the caretaker + l1TriageLabels: + - - 'area: *' + # arrays of labels that determine if an issue has been fully triaged + l2TriageLabels: + - - 'type: bug/fix' + - 'severity*' + - 'freq*' + - 'area: *' + - - 'type: feature' + - 'area: *' + - - 'type: refactor' + - 'area: *' + - - 'type: RFC / Discussion / question' + - 'area: *' + - - 'type: docs' + - 'area: *' + +# Size checking +size: + # Size checking for production build is performed via the E2E test `build/prod-build` + disabled: true + circleCiStatusName: 'ci/circleci: e2e-cli' + maxSizeIncrease: 10000 + comment: false diff --git a/.github/codeql/config.yml b/.github/codeql/config.yml new file mode 100644 index 000000000000..ad81a268eda4 --- /dev/null +++ b/.github/codeql/config.yml @@ -0,0 +1,8 @@ +name: 'Angular CLI CodeQL config' + +query-filters: + # TODO(josephperrott): reevaluate if these can be reenabled. + - exclude: + id: js/bad-code-sanitization + - exclude: + id: js/regex-injection diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..a01353b6bcf0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 + +updates: + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'daily' + commit-message: + prefix: 'build' + labels: + - 'area: build & ci' + - 'target: patch' + - 'action: merge' + # Disable version updates + # This does not affect security updates + open-pull-requests-limit: 0 diff --git a/.github/shared-actions/windows-bazel-test/action.yml b/.github/shared-actions/windows-bazel-test/action.yml new file mode 100644 index 000000000000..524eb1c3a684 --- /dev/null +++ b/.github/shared-actions/windows-bazel-test/action.yml @@ -0,0 +1,78 @@ +name: 'Native Windows Bazel e2e test' +description: 'Runs an Angular CLI e2e Bazel test on native Windows (dispatched from inside WSL)' +author: 'Angular' + +inputs: + test_target_name: + description: E2E test target name + required: true + test_args: + description: | + Text representing the command line arguments that + should be passed to the e2e test runner. + required: false + default: '' + +runs: + using: composite + steps: + - name: Initialize WSL + id: init_wsl + uses: angular/dev-infra/github-actions/setup-wsl@9a3e28a515bf51cd2ecfd5f4d5b17613845e6f44 + with: + wsl_firewall_interface: 'vEthernet (WSL (Hyper-V firewall))' + + - name: Installing pnpm (in WSL) + run: npm install -g pnpm@9 + shell: wsl-bash {0} + + - name: Install node modules in WSL (re-using from previous install/cache restore) + run: | + cd ${{steps.init_wsl.outputs.repo_path}} + pnpm install --frozen-lockfile + shell: wsl-bash {0} + + - name: Build test binary for Windows (inside WSL) + shell: wsl-bash {0} + run: | + cd ${{steps.init_wsl.outputs.repo_path}} + pnpm bazel \ + build --config=e2e //tests/legacy-cli:${{inputs.test_target_name}} --platforms=tools:windows_x64 + env: + # See: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows + WSLENV: 'GOOGLE_APPLICATION_CREDENTIALS/p' + + - name: Copying binary artifact to host + shell: wsl-bash {0} + run: | + cd ${{steps.init_wsl.outputs.repo_path}} + tar -cf /tmp/test.tar.gz dist/bin/tests/legacy-cli/${{inputs.test_target_name}}_ + mkdir /mnt/c/test + mv /tmp/test.tar.gz /mnt/c/test + (cd /mnt/c/test && tar -xf /mnt/c/test/test.tar.gz) + + - name: Convert symlinks for Windows host + shell: wsl-bash {0} + run: | + cd ${{steps.init_wsl.outputs.repo_path}} + + runfiles_dir="/mnt/c/test/dist/bin/tests/legacy-cli/${{inputs.test_target_name}}_/${{inputs.test_target_name}}.bat.runfiles" + + # Make WSL symlinks compatible on Windows native file system. + node scripts/windows-testing/convert-symlinks.mjs $runfiles_dir "${{steps.init_wsl.outputs.cmd_path}}" + + # Needed for resolution because Aspect/Bazel looks for repositories at `/external`. + # TODO(devversion): consult with Aspect on why this is needed. + (cd $runfiles_dir/_main && ${{steps.init_wsl.outputs.cmd_path}} /C "mklink /D external ..") + + - name: Run tests + # Note: This is Git Bash. + shell: bash + env: + BAZEL_BINDIR: '.' + working-directory: "C:\\test" + run: | + node "${{github.workspace}}\\scripts\\windows-testing\\parallel-executor.mjs" \ + $PWD/dist/bin/tests/legacy-cli/${{inputs.test_target_name}}_/${{inputs.test_target_name}}.bat.runfiles \ + ${{inputs.test_target_name}} \ + "${{inputs.test_args}}" \ diff --git a/.github/workflows/assistant-to-the-branch-manager.yml b/.github/workflows/assistant-to-the-branch-manager.yml new file mode 100644 index 000000000000..0753d9c05b1e --- /dev/null +++ b/.github/workflows/assistant-to-the-branch-manager.yml @@ -0,0 +1,21 @@ +name: DevInfra + +on: + push: + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review, labeled] + +# Declare default permissions as read only. +permissions: + contents: read + +jobs: + assistant_to_the_branch_manager: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: angular/dev-infra/github-actions/branch-manager@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000000..d988709a535f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,228 @@ +name: CI + +on: + push: + branches: + - main + - '[0-9]+.[0-9]+.x' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +defaults: + run: + shell: bash + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Generate JSON schema types + # Schema types are required to correctly lint the TypeScript code + run: pnpm admin build-schema + - name: Run ESLint + run: pnpm lint --cache-strategy content + - name: Validate NgBot Configuration + run: pnpm ng-dev ngbot verify + - name: Validate Circular Dependencies + run: pnpm ts-circular-deps check + - name: Run Validation + run: pnpm admin validate + - name: Check tooling setup + run: pnpm check-tooling-setup + + build: + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Build release targets + run: pnpm ng-dev release build + + test: + needs: build + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Run module and package tests + run: pnpm bazel test //modules/... //packages/... + env: + ASPECT_RULES_JS_FROZEN_PNPM_LOCK: '1' + + e2e: + needs: test + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + node: [20, 22] + subset: [npm, esbuild] + shard: [0, 1, 2, 3, 4, 5] + runs-on: ${{ matrix.os }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + - name: Run CLI E2E tests + run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} + + e2e_windows: + strategy: + fail-fast: false + matrix: + os: [windows-2025] + node: [22] + subset: [npm, esbuild] + shard: [0, 1, 2, 3, 4, 5] + runs-on: ${{ matrix.os }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + allow_windows_rbe: true + google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + - name: Run CLI E2E tests + uses: ./.github/shared-actions/windows-bazel-test + with: + test_target_name: e2e.${{ matrix.subset }}_node${{ matrix.node }} + env: + E2E_SHARD_TOTAL: 6 + E2E_SHARD_INDEX: ${{ matrix.shard }} + + e2e-package-managers: + needs: test + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + node: [22] + subset: [yarn, pnpm] + shard: [0, 1, 2] + runs-on: ${{ matrix.os }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + - name: Run CLI E2E tests + run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=3 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} + + e2e-snapshots: + needs: test + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + node: [22] + subset: [npm, esbuild] + shard: [0, 1, 2, 3, 4, 5] + runs-on: ${{ matrix.os }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + - name: Run CLI E2E tests + run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.snapshots.${{ matrix.subset }}_node${{ matrix.node }} + + browsers: + needs: build + runs-on: ubuntu-latest + name: Browser Compatibility Tests + env: + SAUCE_TUNNEL_IDENTIFIER: angular-cli-${{ github.workflow }}-${{ github.run_number }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + - name: Run E2E Browser tests + env: + SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + SAUCE_LOG_FILE: /tmp/angular/sauce-connect.log + SAUCE_READY_FILE: /tmp/angular/sauce-connect-ready-file.lock + SAUCE_PID_FILE: /tmp/angular/sauce-connect-pid-file.lock + SAUCE_TUNNEL_IDENTIFIER: 'angular-${{ github.run_number }}' + SAUCE_READY_FILE_TIMEOUT: 120 + run: | + ./scripts/saucelabs/start-tunnel.sh & + ./scripts/saucelabs/wait-for-tunnel.sh + pnpm bazel test --config=saucelabs //tests/legacy-cli:e2e.saucelabs + ./scripts/saucelabs/stop-tunnel.sh + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: ${{ failure() }} + with: + name: sauce-connect-log + path: ${{ env.SAUCE_CONNECT_DIR_IN_HOST }}/sauce-connect.log + + publish-snapshots: + needs: build + runs-on: ubuntu-latest + env: + CIRCLE_BRANCH: ${{ github.ref_name }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - run: pnpm admin snapshots --verbose + env: + SNAPSHOT_BUILDS_GITHUB_TOKEN: ${{ secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000000..c439b667fb5a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,34 @@ +name: 'CodeQL' + +on: + push: + branches: ['main', '*.*.x'] + schedule: + - cron: '39 9 * * 1' + +permissions: {} + +jobs: + analyze: + name: Analyze + runs-on: 'ubuntu-latest' + permissions: + security-events: write + packages: read + strategy: + fail-fast: false + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Initialize CodeQL + uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + with: + languages: javascript-typescript + build-mode: none + config-file: .github/codeql/config.yml + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + with: + category: '/language:javascript-typescript' diff --git a/.github/workflows/dev-infra.yml b/.github/workflows/dev-infra.yml new file mode 100644 index 000000000000..4e7ca92ff208 --- /dev/null +++ b/.github/workflows/dev-infra.yml @@ -0,0 +1,25 @@ +name: DevInfra + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +# Declare default permissions as read only. +permissions: + contents: read + +jobs: + labels: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: angular/dev-infra/github-actions/commit-message-based-labels@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} + post_approval_changes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: angular/dev-infra/github-actions/post-approval-changes@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/feature-requests.yml b/.github/workflows/feature-requests.yml new file mode 100644 index 000000000000..088bf0e9bb56 --- /dev/null +++ b/.github/workflows/feature-requests.yml @@ -0,0 +1,21 @@ +name: Feature request triage bot + +# Declare default permissions as read only. +permissions: + contents: read + +on: + schedule: + # Run at 13:00 every day + - cron: '0 13 * * *' + +jobs: + feature_triage: + # To prevent this action from running in forks, we only run it if the repository is exactly the + # angular/angular-cli repository. + if: github.repository == 'angular/angular-cli' + runs-on: ubuntu-latest + steps: + - uses: angular/dev-infra/github-actions/feature-request@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml new file mode 100644 index 000000000000..a21fc698a58e --- /dev/null +++ b/.github/workflows/perf.yml @@ -0,0 +1,55 @@ +name: Performance Tracking + +on: + push: + branches: + - main + # Run workflows for all releasable branches + - '[0-9]+.[0-9]+.x' + +permissions: + contents: 'read' + id-token: 'write' + +defaults: + run: + shell: bash + +jobs: + list: + timeout-minutes: 3 + runs-on: ubuntu-latest + outputs: + workflows: ${{ steps.workflows.outputs.workflows }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - id: workflows + run: echo "workflows=$(pnpm -s ng-dev perf workflows --list)" >> "$GITHUB_OUTPUT" + + workflow: + timeout-minutes: 30 + runs-on: ubuntu-latest + needs: list + strategy: + matrix: + workflow: ${{ fromJSON(needs.list.outputs.workflows) }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + # We utilize the google-github-actions/auth action to allow us to get an active credential using workflow + # identity federation. This allows us to request short lived credentials on demand, rather than storing + # credentials in secrets long term. More information can be found at: + # https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-google-cloud-platform + - uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193 # v2.1.10 + with: + project_id: 'internal-200822' + workload_identity_provider: 'projects/823469418460/locations/global/workloadIdentityPools/measurables-tracking/providers/angular' + service_account: 'measures-uploader@internal-200822.iam.gserviceaccount.com' + - run: pnpm ng-dev perf workflows --name ${{ matrix.workflow }} --commit-sha ${{github.sha}} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 000000000000..6e4fddcb8973 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,191 @@ +name: Pull Request + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +defaults: + run: + shell: bash + +jobs: + analyze: + runs-on: ubuntu-latest + outputs: + snapshots: ${{ steps.filter.outputs.snapshots }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + filters: | + snapshots: + - 'tests/legacy-cli/e2e/ng-snapshot/package.json' + + lint: + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup ESLint Caching + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: .eslintcache + key: ${{ runner.os }}-${{ hashFiles('.eslintrc.json') }} + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Generate JSON schema types + # Schema types are required to correctly lint the TypeScript code + run: pnpm admin build-schema + - name: Run ESLint + run: pnpm lint --cache-strategy content + - name: Validate NgBot Configuration + run: pnpm ng-dev ngbot verify + - name: Validate Circular Dependencies + run: pnpm ts-circular-deps check + - name: Run Validation + run: pnpm admin validate + - name: Check Package Licenses + uses: angular/dev-infra/github-actions/linting/licenses@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Check tooling setup + run: pnpm check-tooling-setup + - name: Check commit message + # Commit message validation is only done on pull requests as its too late to validate once + # it has been merged. + run: pnpm ng-dev commit-message validate-range ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} + - name: Check code format + # Code formatting checks are only done on pull requests as its too late to validate once + # it has been merged. + run: pnpm ng-dev format changed --check ${{ github.event.pull_request.base.sha }} + + build: + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Build release targets + run: pnpm ng-dev release build + - name: Store PR release packages + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: packages + path: dist/releases/*.tgz + retention-days: 14 + + test: + needs: build + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Run module and package tests + run: pnpm bazel test //modules/... //packages/... + env: + ASPECT_RULES_JS_FROZEN_PNPM_LOCK: '1' + + e2e: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + node: [22] + subset: [npm, esbuild] + shard: [0, 1, 2, 3, 4, 5] + runs-on: ${{ matrix.os }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Run CLI E2E tests + run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} + + e2e-windows-subset: + needs: build + runs-on: windows-2025 + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + with: + allow_windows_rbe: true + - name: Run CLI E2E tests + uses: ./.github/shared-actions/windows-bazel-test + with: + test_target_name: e2e_node22 + test_args: --esbuild --glob "tests/basic/{build,rebuild}.ts" + + e2e-package-managers: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + node: [22] + subset: [yarn, pnpm] + shard: [0, 1, 2] + runs-on: ${{ matrix.os }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Run CLI E2E tests + run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=3 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} + + e2e-snapshots: + needs: [analyze, build] + if: needs.analyze.outputs.snapshots == 'true' + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + node: [22] + subset: [npm, esbuild] + shard: [0, 1, 2, 3, 4, 5] + runs-on: ${{ matrix.os }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7 + - name: Run CLI E2E tests + run: pnpm bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.snapshots.${{ matrix.subset }}_node${{ matrix.node }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 000000000000..c96366b92885 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,51 @@ +name: OpenSSF Scorecard +on: + branch_protection_rule: + schedule: + - cron: '0 2 * * 0' + push: + branches: [main] + workflow_dispatch: + +# Declare default permissions as read only. +permissions: + contents: read + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results + id-token: write + + steps: + - name: 'Checkout code' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: 'Run analysis' + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + # Upload the results as artifacts. + - name: 'Upload artifact' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: 'Upload to code-scanning' + uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index e6c63302db9c..de3ad9a9154d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,41 @@ bazel-* test-project-host-* dist/ +dist-schema/ + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions # IDEs -.idea/ jsconfig.json + +# Intellij IDEA/WebStorm +# https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.idea/inspectionProfiles/ +.idea/**/compiler.xml +.idea/**/encodings.xml +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Also ignore code styles because .editorconfig is used instead. +.idea/codeStyles/ + +# VSCode +# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore .vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +**/*.code-workspace # Typings file. typings/ @@ -18,6 +48,12 @@ tmp/ npm-debug.log* yarn-error.log* .ng_pkg_build/ +.ng-dev.log +.ng-dev.err*.log +.ng-dev.user* +.husky/_ +.bazelrc.user +.eslintcache # Mac OSX Finder files. **/.DS_Store diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 000000000000..0c6213fc6bb7 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +pnpm -s ng-dev commit-message pre-commit-validate --file $1; diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000000..bbcdc40e0112 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm -s ng-dev format staged; diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg new file mode 100755 index 000000000000..2333b7b798c0 --- /dev/null +++ b/.husky/prepare-commit-msg @@ -0,0 +1 @@ +pnpm -s ng-dev commit-message restore-commit-message-draft $1 $2; diff --git a/.mailmap b/.mailmap index bf8640d5dec9..f651b93dd40b 100644 --- a/.mailmap +++ b/.mailmap @@ -16,6 +16,10 @@ Charles Lyding Charles Lyding <19598772+clydin@users.noreply.github.com> Filipe Silva Mike Brocchi +Alan Agius <17563226+alan-agius4@users.noreply.github.com> + Alan Agius <17563226+alan-agius4@users.noreply.github.com> + Alan Agius <17563226+alan-agius4@users.noreply.github.com> + Alan Agius <17563226+alan-agius4@users.noreply.github.com> ################################################################################ diff --git a/.monorepo.json b/.monorepo.json index 62c5af03b4a4..4d5face644df 100644 --- a/.monorepo.json +++ b/.monorepo.json @@ -1,53 +1,17 @@ { - "badges": [ - [ - { - "image": "https://img.shields.io/circleci/project/github/angular/angular-cli/master.svg?label=circleci", - "label": "CircleCI branch", - "url": "https://circleci.com/gh/angular/angular-cli" - }, - { - "title": "![Dependency Status](https://david-dm.org/angular/angular-cli.svg)", - "url": "https://david-dm.org/angular/angular-cli" - }, - { - "title": "![devDependency Status](https://david-dm.org/angular/angular-cli/dev-status.svg)", - "url": "https://david-dm.org/angular/angular-cli?type=dev" - } - ], - [ - { - "label": "License", - "image": "https://img.shields.io/npm/l/@angular-angular-cli/core.svg", - "url": "https://github.com/angular/angular-cli/blob/master/LICENSE" - } - ], - [ - { - "label": "GitHub forks", - "image": "https://img.shields.io/github/forks/angular/angular-cli.svg?style=social&label=Fork", - "url": "https://github.com/angular/angular-cli/fork" - }, - { - "label": "GitHub stars", - "image": "https://img.shields.io/github/stars/angular/angular-cli.svg?style=social&label=Star", - "url": "https://github.com/angular/angular-cli" - } - ] - ], - "links": { - "Gitter": "https://gitter.im/angular/angular-cli", - "Contributing": "https://github.com/angular/angular-cli/blob/master/CONTRIBUTING.md", - "Angular CLI": "http://github.com/angular/angular-cli" - }, "packages": { - "@_/benchmark": { - "version": "0.7.0-rc.1", - "hash": "a9b1f213a4069f789d20021bda616775" - }, - "devkit": { - "version": "0.7.0-rc.1", - "hash": "37db94c8abaf9d54b3a6267942ed0480" + "@_/builders": {}, + "devkit": {}, + "@angular/build": { + "name": "Angular Build System", + "section": "Tooling", + "links": [ + { + "label": "README", + "url": "/packages/angular/build/README.md" + } + ], + "snapshotRepo": "angular/angular-build-builds" }, "@angular/cli": { "name": "Angular CLI", @@ -55,72 +19,59 @@ "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular/cli/README.md" + "url": "/packages/angular/cli/README.md" } ], - "version": "6.1.0-rc.1", - "snapshotRepo": "angular/cli-builds", - "hash": "c4b9ee68bead42fdfcf1fa6bf49ffabf" + "snapshotRepo": "angular/cli-builds" }, - "@angular/pwa": { - "name": "Angular PWA Schematics", - "section": "Schematics", - "version": "0.7.0-rc.1", - "hash": "9a41163ea10d505e6519fc55814a095b", - "snapshotRepo": "angular/angular-pwa-builds" - }, - "@angular-devkit/architect": { - "name": "Architect", + "@angular/create": { + "name": "Angular Create", + "section": "Misc", "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/architect/README.md" + "url": "/packages/angular/create/README.md" } - ], - "version": "0.7.0-rc.1", - "hash": "e819a187a0de23f0b0dfa20641049e3c", - "snapshotRepo": "angular/angular-devkit-architect-builds" + ] }, - "@angular-devkit/architect-cli": { - "name": "Architect CLI", - "version": "0.7.0-rc.1", - "hash": "692e6210e7ff7bde1251a471db424459", - "snapshotRepo": "angular/angular-devkit-architect-cli-builds" + "@angular/pwa": { + "name": "Angular PWA Schematics", + "section": "Schematics", + "snapshotRepo": "angular/angular-pwa-builds" }, - "@angular-devkit/build-optimizer": { - "name": "Build Optimizer", + "@angular/ssr": { + "name": "Angular SSR", "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_optimizer/README.md" + "url": "/packages/angular/ssr/README.md" } ], - "version": "0.7.0-rc.1", - "hash": "d9a06abe478a77aee9ff3f58b7a03f35", - "snapshotRepo": "angular/angular-devkit-build-optimizer-builds" + "snapshotRepo": "angular/angular-ssr-builds" }, - "@angular-devkit/build-ng-packagr": { - "name": "Build NgPackagr", + "@angular-devkit/architect": { + "name": "Architect", "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_ng_packagr/README.md" + "url": "/packages/angular_devkit/architect/README.md" } ], - "version": "0.7.0-rc.1", - "hash": "da54249267864ef7b8a63a94e24dcc2f", - "snapshotRepo": "angular/angular-devkit-build-ng-packagr-builds" + "snapshotRepo": "angular/angular-devkit-architect-builds" + }, + "@angular-devkit/architect-cli": { + "name": "Architect CLI", + "section": "Tooling", + "snapshotRepo": "angular/angular-devkit-architect-cli-builds" }, "@angular-devkit/build-angular": { "name": "Build Angular", "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_angular/README.md" + "url": "/packages/angular_devkit/build_angular/README.md" } ], - "version": "0.7.0-rc.1", - "hash": "9e2dc84d7345a17654997209398a9092", "snapshotRepo": "angular/angular-devkit-build-angular-builds" }, "@angular-devkit/build-webpack": { @@ -128,23 +79,19 @@ "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_webpack/README.md" + "url": "/packages/angular_devkit/build_webpack/README.md" } ], - "version": "0.7.0-rc.1", - "snapshotRepo": "angular/angular-devkit-build-webpack-builds", - "hash": "ff07cc8534c6eb88dd007fcf39788737" + "snapshotRepo": "angular/angular-devkit-build-webpack-builds" }, "@angular-devkit/core": { "name": "Core", "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/core/README.md" + "url": "/packages/angular_devkit/core/README.md" } ], - "version": "0.7.0-rc.1", - "hash": "4982b3d858dcff4d626679396ecc92a8", "snapshotRepo": "angular/angular-devkit-core-builds" }, "@angular-devkit/schematics": { @@ -152,54 +99,25 @@ "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/schematics/README.md" + "url": "/packages/angular_devkit/schematics/README.md" } ], - "version": "0.7.0-rc.1", - "hash": "f7078fbacc84e356fc9c6f7e25f65ac4", "snapshotRepo": "angular/angular-devkit-schematics-builds" }, "@angular-devkit/schematics-cli": { "name": "Schematics CLI", "section": "Tooling", - "version": "0.7.0-rc.1", - "hash": "327255120822dae24f2f3ee8c126cfa8", "snapshotRepo": "angular/angular-devkit-schematics-cli-builds" }, "@ngtools/webpack": { "name": "Webpack Angular Plugin", - "version": "6.1.0-rc.1", "section": "Misc", - "hash": "7813ed3b234796474aa0c8c61d7682be", "snapshotRepo": "angular/ngtools-webpack-builds" }, "@schematics/angular": { "name": "Angular Schematics", "section": "Schematics", - "version": "0.7.0-rc.1", - "hash": "792227ecdf8dcd7f7f1f4a4552425b22", "snapshotRepo": "angular/schematics-angular-builds" - }, - "@schematics/schematics": { - "name": "Schematics Schematics", - "version": "0.7.0-rc.1", - "section": "Schematics", - "hash": "93c5cca916ad2357e910af128c9fa075", - "snapshotRepo": "angular/schematics-schematics-builds" - }, - "@schematics/package-update": { - "name": "Package JSON Update Schematics", - "version": "0.7.0-rc.1", - "section": "Schematics", - "hash": "8db0dc765453c5b79d4c17a4274af5cc", - "snapshotRepo": "angular/schematics-package-update-builds" - }, - "@schematics/update": { - "name": "Package Update Schematics", - "version": "0.7.0-rc.1", - "section": "Schematics", - "hash": "4afa98b812a74df011cc7349112434a9", - "snapshotRepo": "angular/schematics-update-builds" } } } diff --git a/.ng-dev/caretaker.mjs b/.ng-dev/caretaker.mjs new file mode 100644 index 000000000000..a16e023b1cd0 --- /dev/null +++ b/.ng-dev/caretaker.mjs @@ -0,0 +1,18 @@ +/** + * The configuration for `ng-dev caretaker` commands. + * + * @type { import("@angular/ng-dev").CaretakerConfig } + */ +export const caretaker = { + githubQueries: [ + { + name: 'Merge Queue', + query: `is:pr is:open status:success label:"action: merge"`, + }, + { + name: 'Merge Assistance Queue', + query: `is:pr is:open label:"action: merge-assistance"`, + }, + ], + caretakerGroup: 'angular-cli-caretaker', +}; diff --git a/.ng-dev/commit-message.mjs b/.ng-dev/commit-message.mjs new file mode 100644 index 000000000000..94fe91d8f727 --- /dev/null +++ b/.ng-dev/commit-message.mjs @@ -0,0 +1,15 @@ +import { packages } from '../scripts/packages.mjs'; + +/** + * The configuration for `ng-dev commit-message` commands. + * + * @type { import("@angular/ng-dev").CommitMessageConfig } + */ +export const commitMessage = { + maxLineLength: Infinity, + minBodyLength: 0, + minBodyLengthTypeExcludes: ['docs'], + disallowFixup: true, + // Note: When changing this logic, also change the `contributing.ejs` file. + scopes: packages.map(({ name }) => name), +}; diff --git a/.ng-dev/config.mjs b/.ng-dev/config.mjs new file mode 100644 index 000000000000..6add9773c06c --- /dev/null +++ b/.ng-dev/config.mjs @@ -0,0 +1,6 @@ +export { commitMessage } from './commit-message.mjs'; +export { format } from './format.mjs'; +export { github } from './github.mjs'; +export { pullRequest } from './pull-request.mjs'; +export { release } from './release.mjs'; +export { caretaker } from './caretaker.mjs'; diff --git a/.ng-dev/dx-perf-workflows.yml b/.ng-dev/dx-perf-workflows.yml new file mode 100644 index 000000000000..21c8b95acff3 --- /dev/null +++ b/.ng-dev/dx-perf-workflows.yml @@ -0,0 +1,49 @@ +workflows: + build-cli: + name: '@angular/cli build' + prepare: + - bazel clean + workflow: + - bazel build //packages/angular/cli:npm_package + + angular-build-integration: + name: '@angular/build integration' + disabled: true + prepare: + - bazel clean + workflow: + - bazel test //packages/angular/build:integration_tests + + modules-builder-tests: + name: '@ngtools/webpack test' + prepare: + - bazel clean + workflow: + - bazel test //packages/ngtools/webpack:webpack_test + + devkit-core-tests: + name: '@angular/devkit/core tests' + prepare: + - bazel clean + workflow: + - bazel test //packages/angular_devkit/core:core_test + + devkit-core-tests-rerun: + name: '@angular/devkit/core return test' + prepare: + - bazel clean + workflow: + - bazel test //packages/angular_devkit/core:core_test + # Add a single line to the beginning of a file to trigger a rebuild/retest + - sed -i '1i // comment' packages/angular_devkit/core/src/workspace/core_spec.ts + - bazel test //packages/angular_devkit/core:core_test + cleanup: + # Remove the single line added + - sed -i '1d' packages/angular_devkit/core/src/workspace/core_spec.ts + + build-unit-tests: + name: '@angular/build tests' + prepare: + - bazel clean + workflow: + - bazel test //packages/angular/build:unit_tests diff --git a/.ng-dev/format.mjs b/.ng-dev/format.mjs new file mode 100644 index 000000000000..98115c626abd --- /dev/null +++ b/.ng-dev/format.mjs @@ -0,0 +1,11 @@ +/** + * Configuration for the `ng-dev format` command. + * + * @type { import("@angular/ng-dev").FormatConfig } + */ +export const format = { + 'prettier': { + matchers: ['**/*.{mts,cts,ts,mjs,cjs,js,json,yml,yaml,md}'], + }, + 'buildifier': true, +}; diff --git a/.ng-dev/github.mjs b/.ng-dev/github.mjs new file mode 100644 index 000000000000..15e228fb5ae3 --- /dev/null +++ b/.ng-dev/github.mjs @@ -0,0 +1,12 @@ +/** + * Github configuration for the ng-dev command. This repository is + * used as remote for the merge script. + * + * @type { import("@angular/ng-dev").GithubConfig } + */ +export const github = { + owner: 'angular', + name: 'angular-cli', + mainBranchName: 'main', + useNgDevAuthService: true, +}; diff --git a/.ng-dev/pull-request.mjs b/.ng-dev/pull-request.mjs new file mode 100644 index 000000000000..8beefa10c5fd --- /dev/null +++ b/.ng-dev/pull-request.mjs @@ -0,0 +1,12 @@ +/** + * Configuration for the merge tool in `ng-dev`. This sets up the labels which + * are respected by the merge script (e.g. the target labels). + * + * @type { import("@angular/ng-dev").PullRequestConfig } + */ +export const pullRequest = { + githubApiMerge: { + default: 'rebase', + labels: [{ pattern: 'merge: squash commits', method: 'squash' }], + }, +}; diff --git a/.ng-dev/release.mjs b/.ng-dev/release.mjs new file mode 100644 index 000000000000..3d097ea80b11 --- /dev/null +++ b/.ng-dev/release.mjs @@ -0,0 +1,36 @@ +import semver from 'semver'; +import { releasePackages } from '../scripts/packages.mjs'; + +/** + * Configuration for the `ng-dev release` command. + * + * @type { import("@angular/ng-dev").ReleaseConfig } + */ +export const release = { + representativeNpmPackage: '@angular/cli', + npmPackages: releasePackages.map(({ name, experimental }) => ({ name, experimental })), + buildPackages: async () => { + // The `performNpmReleaseBuild` function is loaded at runtime to avoid loading additional + // files and dependencies unless a build is required. + const { performNpmReleaseBuild } = await import('../scripts/build-packages-dist.mjs'); + return performNpmReleaseBuild(); + }, + prereleaseCheck: async (newVersionStr) => { + const newVersion = new semver.SemVer(newVersionStr); + const { assertValidDependencyRanges } = await import( + '../scripts/release-checks/dependency-ranges/index.mjs' + ); + + await assertValidDependencyRanges(newVersion, releasePackages); + }, + releaseNotes: { + groupOrder: [ + '@angular/cli', + '@schematics/angular', + '@angular-devkit/architect-cli', + '@angular-devkit/schematics-cli', + ], + }, + publishRegistry: 'https://wombat-dressing-room.appspot.com/', + releasePrLabels: ['action: merge'], +}; diff --git a/.ng-dev/tsconfig.json b/.ng-dev/tsconfig.json new file mode 100644 index 000000000000..9f0a0f84be18 --- /dev/null +++ b/.ng-dev/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "resolveJsonModule": true, + "allowJs": true, + "module": "Node16", + "moduleResolution": "Node16", + "checkJs": true, + "noEmit": true, + "types": [] + }, + "include": ["**/*.mjs"], + "exclude": [] +} diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000000..9227ff789b96 --- /dev/null +++ b/.npmrc @@ -0,0 +1,11 @@ +engine-strict = true + +# Disabling pnpm [hoisting](https://pnpm.io/npmrc#hoist) by setting `hoist=false` is recommended on +# projects using rules_js so that pnpm outside of Bazel lays out a node_modules tree similar to what +# rules_js lays out under Bazel (without a hidden node_modules/.pnpm/node_modules) +hoist=false + +# Avoid pnpm auto-installing peer dependencies. We want to be explicit about our versions used +# for peer dependencies, avoiding potential mismatches. In addition, it ensures we can continue +# to rely on peer dependency placeholders substituted via Bazel. +auto-install-peers=false diff --git a/.nvmrc b/.nvmrc index 45a4fb75db86..b8ffd70759fb 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -8 +22.15.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000000..b0b71acaf241 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,18 @@ +/bazel-out/ +/docs/design/analytics.md +/dist-schema/ +/goldens/public-api +/modules/testing/builder/projects/ +/packages/angular_devkit/build_angular/test/ +/packages/angular_devkit/core/src/workspace/json/test/ +/packages/angular_devkit/schematics_cli/blank/project-files/ +/packages/angular_devkit/schematics_cli/blank/schematic-files/ +/packages/angular_devkit/schematics_cli/schematic/files/ +/packages/schematics/angular/third_party/ +/README.md +/CONTRIBUTING.md +.yarn/ +dist/ +/tests/legacy-cli/e2e/assets/ +/tools/test/*.json +pnpm-lock.yaml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000000..22e9e4076522 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 100, + "quoteProps": "preserve", + "singleQuote": true +} diff --git a/BUILD b/BUILD deleted file mode 100644 index 64f2b78d0ee5..000000000000 --- a/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -# 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 -package(default_visibility = ["//visibility:public"]) - -licenses(["notice"]) # MIT License - -exports_files([ - "LICENSE", - "tsconfig.json", # @external -]) - -# NOTE: this will move to node_modules/BUILD in a later release -# @external_begin -filegroup( - name = "node_modules", - srcs = glob(["node_modules/**/*"]), -) -# @external_end diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 000000000000..9518c3a6f0bb --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,96 @@ +load("@aspect_rules_ts//ts:defs.bzl", rules_js_tsconfig = "ts_config") +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") +load("@npm//:defs.bzl", "npm_link_all_packages") +load("//tools:defaults.bzl", "copy_to_bin") + +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +exports_files([ + "LICENSE", + "tsconfig.json", + "tsconfig-test.json", + "tsconfig-build-ng.json", + "tsconfig-build.json", + "package.json", +]) + +npm_link_all_packages() + +rules_js_tsconfig( + name = "build-tsconfig", + src = "tsconfig-build.json", + deps = [ + "tsconfig.json", + "//:node_modules/@types/node", + ], +) + +rules_js_tsconfig( + name = "test-tsconfig", + src = "tsconfig-test.json", + deps = [ + "tsconfig.json", + "//:node_modules/@types/jasmine", + "//:node_modules/@types/node", + ], +) + +rules_js_tsconfig( + name = "build-tsconfig-esm", + src = "tsconfig-build-esm.json", + deps = [ + "tsconfig.json", + ], +) + +rules_js_tsconfig( + name = "test-tsconfig-esm", + src = "tsconfig-test-esm.json", + deps = [ + ":build-tsconfig-esm", + "//:node_modules/@types/jasmine", + "//:node_modules/@types/node", + ], +) + +# Files required by e2e tests +copy_to_bin( + name = "config-files", + srcs = [ + "package.json", + ], +) + +# Detect if the build is running under --stamp +config_setting( + name = "stamp", + values = {"stamp": "true"}, +) + +# If set will replace dependency versions with tarballs for packages in this repo +bool_flag( + name = "enable_package_json_tar_deps", + build_setting_default = False, +) + +config_setting( + name = "package_json_use_tar_deps", + flag_values = { + ":enable_package_json_tar_deps": "true", + }, +) + +# If set will replace dependency versions with snapshot repos for packages in this repo +bool_flag( + name = "enable_snapshot_repo_deps", + build_setting_default = False, +) + +config_setting( + name = "package_json_use_snapshot_repo_deps", + flag_values = { + ":enable_snapshot_repo_deps": "true", + }, +) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000000..d242ff94b916 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16013 @@ + + +# 20.0.0-next.9 (2025-04-30) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [5876577af](https://github.com/angular/angular-cli/commit/5876577af163b534846e720b0184558197dce741) | feat | Add prompt for new apps to be zoneless | +| [c557a19ef](https://github.com/angular/angular-cli/commit/c557a19ef4eed9f2d805bb235d3819c69a1aaef6) | fix | avoid empty polyfill option for new zoneless application | +| [148498c2b](https://github.com/angular/angular-cli/commit/148498c2bcd0feb495dc0aa14b6a4555ac01facb) | fix | Remove experimental from zoneless | +| [0f7dc2cd8](https://github.com/angular/angular-cli/commit/0f7dc2cd8f76f928e64e734563a433ff6a0d478c) | fix | skip spec project reference for minimal ng new | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | +| [d6ea6b09f](https://github.com/angular/angular-cli/commit/d6ea6b09f182433f859a78d4a4d38a9db521e593) | feat | add experimental vitest browser support to unit-testing | +| [05485ede7](https://github.com/angular/angular-cli/commit/05485ede7b472f98120c51f28bd485eeb635bac2) | fix | ensure `com.chrome.devtools.json` is consistently served after initial run | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------- | +| [2d11e8e45](https://github.com/angular/angular-cli/commit/2d11e8e45b29cf879ee72ffbcf438198d73ffaba) | fix | return 302 when redirectTo is a function | + + + + + +# 19.2.10 (2025-04-30) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------- | +| [067f1cba0](https://github.com/angular/angular-cli/commit/067f1cba031361f71c79b70af143c53c777e9f7d) | fix | update vite to 6.2.7 | + + + + + +# 17.3.17 (2025-04-30) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [743d85bac](https://github.com/angular/angular-cli/commit/743d85bacce03bcc454574e0ffa9f243ff6631dd) | fix | update http-proxy-middleware to v2.0.8 | + + + + + +# 20.0.0-next.8 (2025-04-23) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [0ab1ddf63](https://github.com/angular/angular-cli/commit/0ab1ddf632b7305db28a2f87f5c6b099a44669f6) | feat | generate libraries using TypeScript project references | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------- | +| [12def3a2e](https://github.com/angular/angular-cli/commit/12def3a2e907ca8e7d530cea1b39bba90e153144) | feat | add experimental vitest unit-testing support | + + + + + +# 20.0.0-next.7 (2025-04-23) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [e03f2b899](https://github.com/angular/angular-cli/commit/e03f2b89992cb1e34a57f9cd5beef77674c116b6) | feat | Add global error listeners to new app generation | +| [672ae14cd](https://github.com/angular/angular-cli/commit/672ae14cd21d02a3b4727e2febd88747b9e4c684) | fix | drop composite in tsconfig | +| [5e8c6494d](https://github.com/angular/angular-cli/commit/5e8c6494d3eb5a0f61e8b07de4c53233147e9d46) | fix | relative tsconfig paths in references | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [e80963036](https://github.com/angular/angular-cli/commit/e8096303659f4f02ac05fe8f655bb29bc12fda28) | feat | expand browser support policy to widely available Baseline | +| [566de64cb](https://github.com/angular/angular-cli/commit/566de64cbeebeb532db3c0f4ed1dd607c31dedf1) | fix | use virtual module for Karma TestBed initialization | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [8bfcae4c1](https://github.com/angular/angular-cli/commit/8bfcae4c1ba19c9bbd75ddb1ed61ddbf6fa2b76b) | fix | support `getPrerenderParams` for wildcard routes | + + + + + +# 19.2.9 (2025-04-23) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [de52cc2c8](https://github.com/angular/angular-cli/commit/de52cc2c813e49a06828ff9e9ef0543fa63a9929) | fix | update http-proxy-middleware to v3.0.5 | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [cc5229a45](https://github.com/angular/angular-cli/commit/cc5229a4507848d4d2bcf7409ffa56a7c4b2a136) | fix | pass `preserveSymlinks` option to Karma esbuild builder | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [a4e415ea6](https://github.com/angular/angular-cli/commit/a4e415ea6ab204b6d5f5974c6f0a073d66c40faf) | fix | support `getPrerenderParams` for wildcard routes | + + + + + +# 18.2.19 (2025-04-23) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [01cc617bc](https://github.com/angular/angular-cli/commit/01cc617bc0e0a5a30c3b86f679494500a914c574) | fix | update http-proxy-middleware to v3.0.5 | + + + + + +# 20.0.0-next.6 (2025-04-16) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [d6f594fe0](https://github.com/angular/angular-cli/commit/d6f594fe0f8f21d9c0e2abedb5c8433a1aa5c157) | feat | generate applications using TypeScript project references | +| [8654b3fea](https://github.com/angular/angular-cli/commit/8654b3fea4e2ba5af651e6c2a4afddaf6fc42802) | fix | application migration should migrate karma builder package | +| [be6f13ec1](https://github.com/angular/angular-cli/commit/be6f13ec16f01851d38b900dbfc4df7ccfb94d16) | fix | remove setting files tsconfig field with SSR/Server generation | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- | +| [d5a409a79](https://github.com/angular/angular-cli/commit/d5a409a79da16d368a6c0c588f9c987355ead529) | fix | include `module` value check when adding custom conditions | +| [95d16dc52](https://github.com/angular/angular-cli/commit/95d16dc52113a1d5f67c95a5f6d82e5e937f299c) | fix | pass `preserveSymlinks` option to Karma esbuild builder | +| [3d997feb6](https://github.com/angular/angular-cli/commit/3d997feb689b838a9777b7727bf937098c7d5e83) | fix | prevent nested CSS in components | +| [6e6315d72](https://github.com/angular/angular-cli/commit/6e6315d72686a88f29ec9e7565b463e302fdbed8) | fix | properly resolve transitive external dependencies in vite-dev-server | + + + + + +# 19.2.8 (2025-04-16) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- | +| [4a8a4a083](https://github.com/angular/angular-cli/commit/4a8a4a0837af6a095a1e4ad6ae07436073324a7a) | fix | include `module` value check when adding custom conditions | +| [00cd0d123](https://github.com/angular/angular-cli/commit/00cd0d1235ed13781689ae4c4636371dab46b493) | fix | prevent nested CSS in components | +| [a297c4153](https://github.com/angular/angular-cli/commit/a297c4153fd72581cbcf8136c9524c415c561f53) | fix | properly resolve transitive external dependencies in vite-dev-server | +| [8ab033e8e](https://github.com/angular/angular-cli/commit/8ab033e8e56d26c75d8871f81291e702b8985adc) | fix | update vite to 6.2.6 | + + + + + +# 20.0.0-next.5 (2025-04-09) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [fdc6291dd](https://github.com/angular/angular-cli/commit/fdc6291dda4903f418667d415b05367390cf829d) | feat | add update migration to keep previous style guide generation behavior | +| [90615a88b](https://github.com/angular/angular-cli/commit/90615a88b10535d7f0197008b9d48ceac4409c23) | fix | default component templates to not use `.ng.html` extension | +| [5fc595144](https://github.com/angular/angular-cli/commit/5fc5951440c9306c4349fa3f8dbcb1b584441fe8) | fix | generate guards with a dash type separator | +| [040282d8f](https://github.com/angular/angular-cli/commit/040282d8fd5838266785997442c4f5a269666cf3) | fix | generate interceptors with a dash type separator | +| [070d60fb3](https://github.com/angular/angular-cli/commit/070d60fb383bb14d39f969942641253e54980fcf) | fix | generate modules with a dash type separator | +| [e6083b57b](https://github.com/angular/angular-cli/commit/e6083b57bb5b38db14264253095a9729738d22f2) | fix | generate pipes with a dash type separator | +| [92e193c0b](https://github.com/angular/angular-cli/commit/92e193c0b9a2b85b68d83c5f378d30fc8d10f13e) | fix | generate resolvers with a dash type separator | +| [ea1143ddd](https://github.com/angular/angular-cli/commit/ea1143ddd801b775828f0b62788f4cce0dd7e9ce) | fix | infer app component name and path in server schematic | +| [661609e3e](https://github.com/angular/angular-cli/commit/661609e3e583198828baf236338db17b6222f4d8) | fix | set explicit type in library schematic | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [3e24a59a9](https://github.com/angular/angular-cli/commit/3e24a59a9db9f11a80fa616c68be4380c4816ed5) | fix | disable TypeScript `composite` option with Angular compiler | +| [6f913ad5e](https://github.com/angular/angular-cli/commit/6f913ad5e4d8ad9932ef2607851e3b8776e1af3a) | fix | include component test metadata in development builds | +| [fc0e05fea](https://github.com/angular/angular-cli/commit/fc0e05fea89598204a7e5de494da897c396c4e52) | fix | skip normalization of relative externals | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [319b8e0c2](https://github.com/angular/angular-cli/commit/319b8e0c2a0cd30ab96576464b4172a1f76a97a6) | fix | manage unhandled errors in zoneless applications | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [427bd846f](https://github.com/angular/angular-cli/commit/427bd846f552b393cb969472a05488ac11d47e9f) | fix | disable TypeScript composite option with Angular compiler | + + + + + +# 19.2.7 (2025-04-09) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [7f1e8c677](https://github.com/angular/angular-cli/commit/7f1e8c6777dbf60e2a3864774a8c4140bb76f640) | fix | include component test metadata in development builds | +| [74cd4edd5](https://github.com/angular/angular-cli/commit/74cd4edd5bbf5ae03a910be036f6e7fa7db35642) | fix | skip normalization of relative externals | + + + + + +# 18.2.18 (2025-04-09) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [4245ca7b4](https://github.com/angular/angular-cli/commit/4245ca7b434e0aa859c805c459ce50238601b940) | fix | update vite to 5.4.17 | + + + + + +# 17.3.16 (2025-04-09) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------- | +| [5aa53b40c](https://github.com/angular/angular-cli/commit/5aa53b40c34e1555548d201f840a5ffc01f14601) | fix | remove undici from dependencies | +| [fce61564d](https://github.com/angular/angular-cli/commit/fce61564ded8c476ef1c257d2844b1a1560af732) | fix | update vite to 5.4.17 | + + + + + +# 20.0.0-next.4 (2025-04-02) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [1e137ca84](https://github.com/angular/angular-cli/commit/1e137ca848839402bc214fbccdc04243862d01d0) | feat | add migration to update `moduleResolution` to `bundler` | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | +| [4955ee0aa](https://github.com/angular/angular-cli/commit/4955ee0aa31c1021b6369c29a250dd5a9a3f11cd) | fix | properly resolve relative schematics when executed from a nested directory | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [d067cedf0](https://github.com/angular/angular-cli/commit/d067cedf05051e3a0f237d50306e1e4c881a0328) | feat | support custom resolution conditions with applications | +| [8a89438be](https://github.com/angular/angular-cli/commit/8a89438bef66e00d9795a5684c2b91dfdc102b3f) | fix | correctly handle `false` value in server option | +| [52fbffcd7](https://github.com/angular/angular-cli/commit/52fbffcd7bb129720a10e6bf865e4e3a01f939d6) | fix | warn and remove jsdom launcher when used with karma | + + + + + +# 19.2.6 (2025-04-02) + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | +| [e5aec562f](https://github.com/angular/angular-cli/commit/e5aec562feb0d293e88d560ea4ec0720e90dbc11) | fix | properly resolve relative schematics when executed from a nested directory | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [76cfd364a](https://github.com/angular/angular-cli/commit/76cfd364a8b398153c09ce29c5672272ac0bce23) | fix | correctly handle `false` value in server option | +| [d69188c6b](https://github.com/angular/angular-cli/commit/d69188c6be2b851e3dfb84e2bd8d209062d7a283) | fix | update vite to 6.2.4 due to a security issues | + + + + + +# 18.2.17 (2025-04-02) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [247ceff7f](https://github.com/angular/angular-cli/commit/247ceff7f7d71901f51dbab1c1a5235d59e45847) | fix | update vite to 5.4.16 due to a security issues | + + + + + +# 17.3.15 (2025-04-02) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [0525fec61](https://github.com/angular/angular-cli/commit/0525fec6183c2972b97a6ad4d57e89aaa478a2de) | fix | update vite to 5.4.16 due to a security issues | + + + + + +# 20.0.0-next.3 (2025-03-26) + +## Breaking Changes + +### @angular/cli + +- Node.js versions from 22.0 to 22.10 are no longer supported + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [9e6b9b537](https://github.com/angular/angular-cli/commit/9e6b9b5379d0448578b3bfb6100852dea7febe75) | fix | add type checking of host bindings to strict config | +| [381d35fe4](https://github.com/angular/angular-cli/commit/381d35fe40f062713eac550a12b58c30c1ec33a9) | fix | remove empty `scripts` option value from new applications | +| [a910fe9ae](https://github.com/angular/angular-cli/commit/a910fe9ae0423146f6509c5b9c45c88415365c9f) | fix | remove explicit `outputPath` option value from generated applications | + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [64732534e](https://github.com/angular/angular-cli/commit/64732534ecb84d702bde2469466a05e765879f9a) | fix | update minimum supported Node.js 22 version to 22.11.0 | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [c1de63300](https://github.com/angular/angular-cli/commit/c1de633007c423cfd9113cc781b5647e59306146) | feat | allow control of source map sources content for application builds | +| [9b682e625](https://github.com/angular/angular-cli/commit/9b682e62519e761477e6266650239bf58026a9f4) | feat | support a default outputPath option for applications | +| [156a14e38](https://github.com/angular/angular-cli/commit/156a14e387d83002fa01b33d574a6fbc078dad84) | fix | correct handling of response/request errors | +| [a8817a3b2](https://github.com/angular/angular-cli/commit/a8817a3b2a9a94bdfcba4bf690e217e7d2d4686c) | fix | handle undefined `getOrCreateAngularServerApp` during error compilation | +| [bd917d92a](https://github.com/angular/angular-cli/commit/bd917d92a653b1a5ece7ab96adfde8f8d282c34a) | fix | normalize karma asset paths before lookup | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [63428f3f1](https://github.com/angular/angular-cli/commit/63428f3f1e2ffd427011ea8a17b70f8829ae0bdf) | perf | flush headers prior to start rendering the HTML | +| [6bd7b9b4a](https://github.com/angular/angular-cli/commit/6bd7b9b4a59240caa4f19185570aec8263d8a0a7) | perf | optimized request handling performance | + + + + + +# 19.2.5 (2025-03-26) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [20455e2a6](https://github.com/angular/angular-cli/commit/20455e2a64558fcbb11906cb414a99d3976645d6) | fix | correct handling of response/request errors | +| [32b1dcd91](https://github.com/angular/angular-cli/commit/32b1dcd91b9f351bb6baa54f52c81c465185e01b) | fix | handle undefined `getOrCreateAngularServerApp` during error compilation | +| [7552a9fec](https://github.com/angular/angular-cli/commit/7552a9fec971f64ff27d78754ed13654e9a56b43) | fix | normalize karma asset paths before lookup | +| [1eb5b4357](https://github.com/angular/angular-cli/commit/1eb5b43575ab9908122606b94c0aaa53718678aa) | fix | update vite to 6.2.3 | + + + + + +# 18.2.16 (2025-03-26) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [4267a80c5](https://github.com/angular/angular-cli/commit/4267a80c5cd1e9e6aaae0f9090e21c2d71a6887f) | fix | remove `@vitejs/plugin-basic-ssl` from dependencies | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [9c2904d0d](https://github.com/angular/angular-cli/commit/9c2904d0d3a7b2790b27d21c1ff23e6d8a01c4f0) | fix | update vite to 5.4.15 | + + + + + +# 17.3.14 (2025-03-26) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [cb8f859f1](https://github.com/angular/angular-cli/commit/cb8f859f181a325c15b91791c78f5326f22bb7f5) | fix | update vite to 5.4.15 | + + + + + +# 20.0.0-next.2 (2025-03-19) + +## Breaking Changes + +### @schematics/angular + +- `--server-routing` option has been removed from several schematics. Server routing will be used when using the application builder. + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------- | +| [26fd4ea73](https://github.com/angular/angular-cli/commit/26fd4ea73ad2a0148ae587d582134c68a0bf4b86) | feat | add migrations for server rendering updates | +| [18e13e2ce](https://github.com/angular/angular-cli/commit/18e13e2ceed931d29aa5582980c7d6d1f66c9787) | feat | remove `--server-routing` option | +| [86d241629](https://github.com/angular/angular-cli/commit/86d241629ff51f0bb5988e81cac8658b01704d49) | fix | add `@angular/ssr` dependency only when `provideServerRendering` import has been updated | +| [da6ef626f](https://github.com/angular/angular-cli/commit/da6ef626f960b187a7862f0caa3d8aed38224ac2) | fix | ensure app-shell schematic consistently uses `withAppShell` | +| [f126f8d34](https://github.com/angular/angular-cli/commit/f126f8d34b087dd3a916dfb93cd255aac4d6c309) | fix | ensure module discovery checks for an NgModule decorator | +| [23fc8e1e1](https://github.com/angular/angular-cli/commit/23fc8e1e176f23442876b086bff52dd5f35abbc0) | fix | generate components without a `.component` extension/type | +| [8d715fa94](https://github.com/angular/angular-cli/commit/8d715fa948d432b18d06bcf42eed3a7681383523) | fix | generate directives without a .directive extension/type | +| [bc0f07b48](https://github.com/angular/angular-cli/commit/bc0f07b484300848ee81c5719c58909b40f99deb) | fix | generate services without a .service extension/type | +| [c0de72317](https://github.com/angular/angular-cli/commit/c0de723173549f62a524b6e6c58c6d80c8054581) | fix | replace `@angular/platform-browser-dynamic` with `@angular/platform-browser` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [f4be83119](https://github.com/angular/angular-cli/commit/f4be831197010a17394264bc74b1eb385ba95028) | feat | Support Sass package importers | +| [cb2ab43ab](https://github.com/angular/angular-cli/commit/cb2ab43abcf0e3c1a2cc584a326e1eea5eede7a8) | fix | ensure errors for missing component resources | +| [f780e8beb](https://github.com/angular/angular-cli/commit/f780e8beb3ccea27ef0442d1d3814ec2a668057d) | fix | ensure relative karma stack traces for test failures | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------- | +| [33b9de3eb](https://github.com/angular/angular-cli/commit/33b9de3eb1fa596a4d5a975d05275739f2f7b8ae) | feat | expose `provideServerRendering` and remove `provideServerRouting` | +| [cdfc50c29](https://github.com/angular/angular-cli/commit/cdfc50c29a2aa6f32d172b505a0ef09e563dfc59) | feat | stabilize `AngularNodeAppEngine`, `AngularAppEngine`, and `provideServerRouting` APIs | + + + + + +# 19.2.4 (2025-03-19) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------- | +| [0a4e96bda](https://github.com/angular/angular-cli/commit/0a4e96bda054876332c5603a3bc972c3ec1eb0bf) | fix | replace `@angular/platform-browser-dynamic` with `@angular/platform-browser` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [b0b643e46](https://github.com/angular/angular-cli/commit/b0b643e46f1009be66423fdff568d042717c5e2b) | fix | ensure errors for missing component resources | +| [2cd763e89](https://github.com/angular/angular-cli/commit/2cd763e893788cfb38260d48eef40afa574a6a70) | fix | ensure relative karma stack traces for test failures | + + + + + +# 17.3.13 (2025-03-13) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [22901df02](https://github.com/angular/angular-cli/commit/22901df0261812a3408ff9d7a7690bf6b87ec2a3) | fix | update babel packages | + + + + + +# 19.2.3 (2025-03-13) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [5a739820b](https://github.com/angular/angular-cli/commit/5a739820be5cc7cb25e159a1f2283db92e741f78) | fix | update babel packages | + + + + + +# 18.2.15 (2025-03-13) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [255c8a50d](https://github.com/angular/angular-cli/commit/255c8a50d2214747c8121e963afcd96cbff39293) | fix | update babel packages | + + + + + +# 20.0.0-next.1 (2025-03-13) + +## Breaking Changes + +### @angular/build + +- TypeScript versions less than 5.8 are no longer supported. + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [03180fe03](https://github.com/angular/angular-cli/commit/03180fe0358662f8fd3255ad546994da3e3bda9c) | feat | use TypeScript module preserve option for new projects | +| [dc2f65999](https://github.com/angular/angular-cli/commit/dc2f65999a64453a26b61c96080b732fdc4147c8) | fix | generate component templates with a `.ng.html` file extension | + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [2d03d8f11](https://github.com/angular/angular-cli/commit/2d03d8f11325cfba72b43f531e4bc27140d45caf) | fix | record analytics for nested schematics | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [31c81e9c6](https://github.com/angular/angular-cli/commit/31c81e9c6859e68d00828b345d996d1aff431b25) | feat | drop support for TypeScript older than 5.8 | +| [3c9172159](https://github.com/angular/angular-cli/commit/3c9172159c72f3c8ea116557ba5bf917a15d2f07) | feat | integrate Chrome automatic workspace folders | +| [f0dd60be1](https://github.com/angular/angular-cli/commit/f0dd60be1ec72d9c8674471965b11be83083a0f1) | fix | exclude all entrypoints of a library from prebundling | +| [3e3516785](https://github.com/angular/angular-cli/commit/3e35167855b3eacb9f45948ef75e999956819490) | fix | handle postcss compilation errors gracefully | +| [5bea3de4c](https://github.com/angular/angular-cli/commit/5bea3de4cb2ffa26ad04aced22be3ff11f519f92) | fix | invalidate `com.chrome.devtools.json` if project is moved | +| [b100c71cc](https://github.com/angular/angular-cli/commit/b100c71ccd39ff62203f16cbe543ba77b98bbe1d) | fix | provide `extract-i18n` does not respect | +| [beab546bf](https://github.com/angular/angular-cli/commit/beab546bf2680d568af12e51e948a100098ae3fd) | fix | remove duplicate prebundling warning | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [ee8466de5](https://github.com/angular/angular-cli/commit/ee8466de520c3db08579be376dbd2b98795f50a8) | fix | prevent stream draining if `write` does not return a boolean | + + + + + +# 19.2.2 (2025-03-12) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [0ee24e29b](https://github.com/angular/angular-cli/commit/0ee24e29b9bb24e92ca3159a13a21fac78974fd7) | fix | record analytics for nested schematics | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [4575265f0](https://github.com/angular/angular-cli/commit/4575265f0b6dcfe81a729f60264e148d93302a10) | fix | exclude all entrypoints of a library from prebundling | +| [83fcffbb7](https://github.com/angular/angular-cli/commit/83fcffbb7d2ede1b08b4145dcedd46ef328bb2f8) | fix | handle postcss compilation errors gracefully | +| [78297ee47](https://github.com/angular/angular-cli/commit/78297ee47c9c381b08cd3649d369765c0b73d4f9) | fix | provide `extract-i18n` does not respect | +| [b18b9c8f2](https://github.com/angular/angular-cli/commit/b18b9c8f249df7b79caebc5ffca07198c14b9a72) | fix | remove duplicate prebundling warning | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [e6e8ce960](https://github.com/angular/angular-cli/commit/e6e8ce960a8048e7bfbaafa4ea013bb05d9897aa) | fix | prevent stream draining if `write` does not return a boolean | + + + + + +# 20.0.0-next.0 (2025-03-05) + +## Breaking Changes + +### @angular/cli + +- Node.js v18 is no longer supported with Angular. + + Before updating a project to Angular v20, the Node.js version must be + at least 20.11.1. For the full list of supported Node.js versions, + see https://angular.dev/reference/versions. + +### @angular-devkit/schematics + +- The `NodePackageLinkTask` has been removed without a replacement. Create a custom task if needed. + + Note: This does not affect application developers. + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------- | +| [093c5a315](https://github.com/angular/angular-cli/commit/093c5a3152c4282d4afb51df40945283cc94d281) | feat | directly use `@angular/build` in new projects | + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------- | +| [5e90c1b4e](https://github.com/angular/angular-cli/commit/5e90c1b4ec3f1d05ad00f2f854347a5bf8cb0860) | fix | remove Node.js v18 support | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------- | +| [e6be37601](https://github.com/angular/angular-cli/commit/e6be37601d57f884a1ddf2cc1ddecf51819b9f51) | refactor | remove deprecated `NodePackageLinkTask` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [c8c73185a](https://github.com/angular/angular-cli/commit/c8c73185a66c7c7825e30f7fcedbaacc9ca1c593) | fix | ensure matching coverage excludes with karma on Windows | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [280693231](https://github.com/angular/angular-cli/commit/280693231e143aa09f841e3179317573a3576545) | perf | optimize response times by introducing header flushing | + + + + + +# 19.2.1 (2025-03-05) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------- | +| [4c35b5721](https://github.com/angular/angular-cli/commit/4c35b5721b146d3c27f200c2688073c20dbe0a19) | fix | prevent accidental deletion of `main.ts` during application builder migration | +| [d7f9cb578](https://github.com/angular/angular-cli/commit/d7f9cb578d164aba830751cffb035bf8d962eca2) | fix | prevent error when tsconfig file is missing in application builder migration | + +### @angular-devkit/architect + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [3ebd7ca7c](https://github.com/angular/angular-cli/commit/3ebd7ca7caeb266308856f47af06bea641b1f8e8) | fix | improve error message when configuration is missing | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [c07330967](https://github.com/angular/angular-cli/commit/c0733096797d45a5cd3ffc18f89a5c75a521accb) | fix | allow component HMR with a service worker | +| [c989c91c3](https://github.com/angular/angular-cli/commit/c989c91c37cab9571bdfaa91cbd806acd9cf9d19) | fix | exclude component styles from 'any' and 'all' budget calculations | +| [96e5dcb5f](https://github.com/angular/angular-cli/commit/96e5dcb5f14b8d16520974b80bb531a190be2343) | fix | handle undefined `less` stylesheet sourcemap values | + + + + + +# 19.2.0 (2025-02-26) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [fe8d83a1f](https://github.com/angular/angular-cli/commit/fe8d83a1f6b5e212d6d51d8f042141c3792ed1cf) | fix | add additional checks for application builder usage | +| [adf4ea5d4](https://github.com/angular/angular-cli/commit/adf4ea5d4ccb252132301111153619178c5bdabe) | fix | remove animations module from ng new app | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------- | +| [ef7ea536f](https://github.com/angular/angular-cli/commit/ef7ea536feae128b9fabaa124cde2bdad3802cba) | feat | add aot option to jest | +| [523d539c6](https://github.com/angular/angular-cli/commit/523d539c6633ab223723162f425e0ef2b7b4ff71) | feat | add aot option to karma | +| [a00a49a65](https://github.com/angular/angular-cli/commit/a00a49a65ae68e6e0f9856d8d0f4d9914031cd05) | feat | add aot to WTR schema | +| [2bae1a9c0](https://github.com/angular/angular-cli/commit/2bae1a9c0c9eff8087b67c7890b87dc1c279c809) | fix | support aot option for karma browser builder | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [11fab9c7d](https://github.com/angular/angular-cli/commit/11fab9c7dde950e46b2a23d239bb9e29b20f5eff) | feat | add application builder karma testing to package | +| [a5fcf8044](https://github.com/angular/angular-cli/commit/a5fcf804428b835cd79bd8fad55c16e614c2be3a) | fix | provide karma stack trace sourcemap support | +| [964fb778b](https://github.com/angular/angular-cli/commit/964fb778b7d9e4811a6987eddc4f0a010bb713f6) | fix | support per component updates of multi-component files | +| [f836be9e6](https://github.com/angular/angular-cli/commit/f836be9e676575fccd4d74eddbc5bf647f7ff1bd) | fix | support Vite `allowedHosts` option for development server | +| [0ddf6aafa](https://github.com/angular/angular-cli/commit/0ddf6aafaa65b3323dc4ee6251d75794ae862ec7) | fix | utilize bazel stamp instead of resolving peer dependency versions | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- | +| [9726cd084](https://github.com/angular/angular-cli/commit/9726cd084b76fe605405d562a18d8af91d6657d8) | feat | Add support for route matchers with fine-grained render mode control | + + + + + +# 19.1.9 (2025-02-26) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- | +| [2d361e9b0](https://github.com/angular/angular-cli/commit/2d361e9b0ae5409d7ab23f50b089da16497623c1) | fix | always disable JSON stats with dev-server | + + + + + +# 19.1.8 (2025-02-19) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | +| [f76cee637](https://github.com/angular/angular-cli/commit/f76cee6378d1fb103a47c4c9006df344029491c9) | fix | correctly parse and resolve relative schematic collection names on Windows | +| [ceba7739c](https://github.com/angular/angular-cli/commit/ceba7739cc72835d080a3c2246209a635212a607) | fix | prefer installed package as fallback when listing package groups | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------- | +| [c54b9996a](https://github.com/angular/angular-cli/commit/c54b9996adb23ebc0a5e1e159ac4a9b54cbf2f1a) | fix | pass missing options to Karma esbuild builder | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [2f60a24dd](https://github.com/angular/angular-cli/commit/2f60a24dd76b3345aef666e7a84099863349c53e) | fix | suppress asset missing warning for `/index.html` requests | +| [b8f7952b7](https://github.com/angular/angular-cli/commit/b8f7952b783a83649364107c78f0fb87ac7b3cf3) | fix | update critical CSS inlining to support `autoCsp` | + + + + + +# 19.1.7 (2025-02-12) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [de73b1c0c](https://github.com/angular/angular-cli/commit/de73b1c0c2d5748818d2e94f93f2640d4c6b949c) | fix | include default export for Express app | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [8890a5f76](https://github.com/angular/angular-cli/commit/8890a5f76c252fe383a632880df476e5f63ef931) | fix | always provide Vite client helpers with development server | +| [df1d38846](https://github.com/angular/angular-cli/commit/df1d388465b6f0d3aab5fb4f011cbbe74d3058f4) | fix | configure Vite CORS option | +| [a13a49d95](https://github.com/angular/angular-cli/commit/a13a49d95be61d2a2458962d57318f301dede502) | fix | exclude unmodified files from logs with `--localize` | +| [0826315fa](https://github.com/angular/angular-cli/commit/0826315fac1c3fd2d22aa0ea544bd59ef9ed8781) | fix | handle unlocalizable files correctly in localized prerender | +| [d2e1c8e9f](https://github.com/angular/angular-cli/commit/d2e1c8e9f5c03a410d8204a5f9b11b4ad9cc9eaa) | perf | cache translated i18n bundles for faster builds | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [f5d974576](https://github.com/angular/angular-cli/commit/f5d97457622897b41e73a859dd1f218fa962be15) | fix | accurately calculate content length for static pages with `\r\n` | +| [c26ea1619](https://github.com/angular/angular-cli/commit/c26ea1619095102b21176435af826cf53f0054b1) | fix | properly handle baseHref with protocol | + + + + + +# 17.3.12 (2025-02-12) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------- | +| [d83237028](https://github.com/angular/angular-cli/commit/d832370285adccbf955963a5115cf9b9bf54a08d) | fix | update vite to version 5.4.14 | + + + + + +# 19.1.6 (2025-02-05) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [3f7042672](https://github.com/angular/angular-cli/commit/3f704267223d1881ea40e9de4e6381b9d0e43fe6) | fix | remove additional newline after standalone property | +| [e9778dba0](https://github.com/angular/angular-cli/commit/e9778dba0d75e7f528b600d51504a583485bd033) | fix | skip ssr migration when `@angular/ssr` is not a dependency | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------- | +| [27f833186](https://github.com/angular/angular-cli/commit/27f8331865de35044ddeda7a8c05bb2700b0be6a) | fix | avoid pre-transform errors with Vite pre-bundling | +| [8f6ee7ed9](https://github.com/angular/angular-cli/commit/8f6ee7ed933ea7394e14fe46d141427839008040) | fix | ensure full rebuild after initial error build in watch mode | +| [2b9c00f68](https://github.com/angular/angular-cli/commit/2b9c00f686145a8613dc2ce7f494193622e02625) | fix | prevent fallback to serving main.js for unknown requests | +| [45abd15b7](https://github.com/angular/angular-cli/commit/45abd15b781bb5bb067a7a52e7a809bb9d141c75) | fix | prevent server manifest generation when no server features are enabled | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [5bf5e5fd2](https://github.com/angular/angular-cli/commit/5bf5e5fd20e3c33a274a936dd1ce00e07b860226) | fix | prioritize the first matching route over subsequent ones | + + + + + +# 19.1.5 (2025-01-29) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | +| [14e3a71e4](https://github.com/angular/angular-cli/commit/14e3a71e46e12a556323fff48998794eecab9896) | fix | update library schematic to use `@angular-devkit/build-angular:ng-packagr` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- | +| [d53d25fc1](https://github.com/angular/angular-cli/commit/d53d25fc1b80388158643dbdd37aa49b0aa790e0) | fix | allow tailwindcss 4.x as a peer dependency | +| [bd9d379f0](https://github.com/angular/angular-cli/commit/bd9d379f0401a19d527dc896a96b2671b4c4ed76) | fix | disable TypeScript `removeComments` option | +| [e73e9102e](https://github.com/angular/angular-cli/commit/e73e9102e3098882dd76a8dbf800d4ba414e0e27) | fix | handle empty module case to avoid TypeError | +| [bb413456e](https://github.com/angular/angular-cli/commit/bb413456e95a9be49feba95415915ce2ef39b1b5) | fix | keep background referenced HMR update chunks | +| [2011d3428](https://github.com/angular/angular-cli/commit/2011d34286784156b8c09bb8c6d376d8f902bc00) | fix | support template updates that also trigger global stylesheet changes | +| [688019946](https://github.com/angular/angular-cli/commit/688019946358b176eacc872ece72987387a603f1) | fix | update vite to version 6.0.11 | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------ | +| [94643d54d](https://github.com/angular/angular-cli/commit/94643d54da1ddadcec1c169aa844a716bec612f6) | fix | enhance dynamic route matching for better performance and accuracy | +| [747557aa0](https://github.com/angular/angular-cli/commit/747557aa0aad00f1df2ce7912ab49775e19c60dc) | fix | redirect to locale pathname instead of full URL | +| [bbbc1eb7a](https://github.com/angular/angular-cli/commit/bbbc1eb7a0c295e0bc4aea95b7292ba484f8a360) | fix | rename `provideServerRoutesConfig` to `provideServerRouting` | + + + + + +# 18.2.14 (2025-01-29) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------- | +| [9d34d28ec](https://github.com/angular/angular-cli/commit/9d34d28ec2965e1b9753556b2721d25ab05c655b) | fix | remove unused `vite` dependency | + + + + + +# 18.2.13 (2025-01-29) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [deeaf1883](https://github.com/angular/angular-cli/commit/deeaf18836efddfa1ee56a25e44944ba444d35ac) | fix | correctly select package versions in descending order during `ng add` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------- | +| [fdddf2c08](https://github.com/angular/angular-cli/commit/fdddf2c0844081667a09f2ffe0b16f77384959b2) | fix | update vite to version 5.4.14 | + + + + + +# 19.1.4 (2025-01-22) + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [aa6f0d051](https://github.com/angular/angular-cli/commit/aa6f0d051179d31aad2c3be7b79f9fda8de60f34) | fix | ensure collections can be resolved via test runner in pnpm workspaces | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------------- | +| [ff8192a35](https://github.com/angular/angular-cli/commit/ff8192a355ca38edb34fb0cfe08ef133629f3f63) | fix | correct path for `/@ng/components` on Windows | +| [14d2f7ca0](https://github.com/angular/angular-cli/commit/14d2f7ca0e930fceeea53d307db79f0e963c1d53) | fix | include extracted routes in the manifest during prerendering | +| [c87a38f5b](https://github.com/angular/angular-cli/commit/c87a38f5b25b3cddd1b2a1ee4b443b10cf03b767) | fix | only issue invalid i18n config error for duplicate `subPaths` with inlined locales | +| [d50788cf9](https://github.com/angular/angular-cli/commit/d50788cf9f799ffbe9ba0edde425e6833623686d) | fix | replace deprecation of `i18n.baseHref` with a warning | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- | +| [bcc5fab75](https://github.com/angular/angular-cli/commit/bcc5fab750c0029e16ad91d277f88113a60b7fa1) | fix | prevent route matcher error when SSR routing is not used | +| [9bacf3981](https://github.com/angular/angular-cli/commit/9bacf3981995626cf935cf1620c391338de1c9df) | fix | properly manage catch-all routes with base href | +| [59c757781](https://github.com/angular/angular-cli/commit/59c75778112383816da2f729d5ae80705b5828fa) | fix | unblock route extraction with `withEnabledBlockingInitialNavigation` | + + + + + +# 19.1.3 (2025-01-20) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [7d8c34172](https://github.com/angular/angular-cli/commit/7d8c34172bf29fbf61c0c0114c419903804b6b38) | fix | allow asset changes to reload page on incremental updates | +| [9fa29af37](https://github.com/angular/angular-cli/commit/9fa29af374060a05a19b32d1f0dee954ec70f451) | fix | handle relative `@ng/components` | +| [c4de34703](https://github.com/angular/angular-cli/commit/c4de34703f8b17ac96e66f889fa0e3ffff524831) | fix | perform full reload for templates with `$localize` usage | + + + + + +# 19.1.2 (2025-01-17) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------ | +| [939d1612a](https://github.com/angular/angular-cli/commit/939d1612add13bab9aed6cc77bce0e17555bfe3b) | fix | perform incremental background file updates with component updates | +| [304027207](https://github.com/angular/angular-cli/commit/30402720707b7a8b9042a6046692d62a768cdc64) | fix | prevent full page reload on HMR updates with SSR enabled | +| [148acbd58](https://github.com/angular/angular-cli/commit/148acbd58a13b1ba8c4a3349bd6042c24a9f93b5) | fix | reset component updates on dev-server index request | + + + + + +# 19.1.1 (2025-01-16) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | +| [298506751](https://github.com/angular/angular-cli/commit/298506751f2b3788fa2def7f7b4012e9e5465047) | fix | resolve HMR-prefixed files in SSR with Vite | + + + + + +# 19.1.0 (2025-01-15) + +## Deprecations + +### @angular/build + +- The `baseHref` option under `i18n.locales` and `i18n.sourceLocale` in `angular.json` is deprecated in favor of `subPath`. + + The `subPath` defines the URL segment for the locale, serving as both the HTML base HREF and the directory name for output. By default, if not specified, `subPath` will use the locale code. + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [02825eec5](https://github.com/angular/angular-cli/commit/02825eec53456384ba5b9c19f25dde3cfc95d796) | feat | use `@angular/build` package in library generation schematic | +| [88431b756](https://github.com/angular/angular-cli/commit/88431b7564d6757898744597a67fcdc178413128) | fix | application migration should migrate ng-packagr builder package | + +### @angular-devkit/architect + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [2b8a02bac](https://github.com/angular/angular-cli/commit/2b8a02bac098d4ac4f31b0e74bedfc739171e30b) | feat | require build schemas from modules | +| [fe1ae6933](https://github.com/angular/angular-cli/commit/fe1ae6933998104c144b2c8854f362289c8d91c6) | fix | avoid Node.js resolution for relative builder schema | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [ce7c4e203](https://github.com/angular/angular-cli/commit/ce7c4e203d0312d21d4a3d1955f9c97bdf3e06d2) | fix | handle Windows drive letter case insensitivity in path functions | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [2f55209dd](https://github.com/angular/angular-cli/commit/2f55209dd24602bdf8c27ef083f96b5f55166971) | fix | update `Rule` type to support returning a `Promise` of `Tree` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------ | +| [2c9d7368f](https://github.com/angular/angular-cli/commit/2c9d7368fc30f3488152e35ac468db5f2a9432f2) | feat | add ng-packagr builder to the package | +| [0a570c0c2](https://github.com/angular/angular-cli/commit/0a570c0c2e64c61ce9969975a21c0d9aac8d9f3b) | feat | add support for customizing URL segments with i18n | +| [298b554a7](https://github.com/angular/angular-cli/commit/298b554a7a40465444b4c508e2250ecbf459ea47) | feat | enable component template hot replacement by default | +| [d350f357b](https://github.com/angular/angular-cli/commit/d350f357b2a74df828ec022e03754d59cc680848) | fix | correctly validate locales `subPath` | +| [8aa1ce608](https://github.com/angular/angular-cli/commit/8aa1ce60808c073634237d03045626d379a51183) | fix | handle loaders correctly in SSR bundles for external packages | +| [3b7e6a8c6](https://github.com/angular/angular-cli/commit/3b7e6a8c6e2e018a85b437256040fd9c8161d537) | fix | invalidate component template updates with dev-server SSR | +| [8fa682e57](https://github.com/angular/angular-cli/commit/8fa682e571dbba4bf249ceb3ca490c4ddd4d7fe5) | fix | remove deleted assets from output during watch mode | +| [48cae815c](https://github.com/angular/angular-cli/commit/48cae815cfd0124217c1b5bc8c92dfdb0b150101) | fix | skip vite SSR warmup file configuration when SSR is disabled | +| [ba16ad6b5](https://github.com/angular/angular-cli/commit/ba16ad6b56e9a1ae0f380141bc1e1253a75fcf6b) | fix | support incremental build file results in watch mode | +| [955acef3d](https://github.com/angular/angular-cli/commit/955acef3d504ac924bd813f401fa9b49edbd337b) | fix | trigger browser reload on asset changes with Vite dev server | +| [e74300a2c](https://github.com/angular/angular-cli/commit/e74300a2cbc666482992fa8d6dbfeef37f3a9db5) | fix | use component updates for component style HMR | +| [6a19c217e](https://github.com/angular/angular-cli/commit/6a19c217eaebf9c0bffba8482545efc375fd2a8a) | fix | warn when using both `isolatedModules` and `emitDecoratorMetadata` | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------- | +| [8d7a51dfc](https://github.com/angular/angular-cli/commit/8d7a51dfc9658aa2f0f0c527435c05c2b10f34e5) | feat | add `modulepreload` for lazy-loaded routes | +| [41ece633b](https://github.com/angular/angular-cli/commit/41ece633b3d42ef110bf6085fe0783ab2a56efcd) | feat | redirect to preferred locale when accessing root route without a specified locale | +| [3feecddbb](https://github.com/angular/angular-cli/commit/3feecddbba0d0559da10a45ad4040faf8e9d5198) | fix | disable component boostrapping when running route extraction | +| [6edb90883](https://github.com/angular/angular-cli/commit/6edb90883733040d77647cf24dea7f53b1b6ceaa) | fix | throw error when using route matchers | + + + + + +# 19.0.7 (2025-01-08) + +### @angular-devkit/architect + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [95c22aeff](https://github.com/angular/angular-cli/commit/95c22aeff4560a199416a20c3622301c5c690119) | fix | provide better error when builder is not defined | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------- | +| [028652992](https://github.com/angular/angular-cli/commit/028652992f0f9cc6fec5de35be7ecf74ec3947c5) | fix | preserve css type for jasmine.css | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [f7522342a](https://github.com/angular/angular-cli/commit/f7522342a8dd99543422629db6dcf1228c5d7279) | fix | add asset tracking to application builder watch files | +| [e973643bf](https://github.com/angular/angular-cli/commit/e973643bfbe47447ca522ca59b07a94fe6c03e0b) | fix | do not mark Babel \_defineProperty helper function as pure | +| [881095eec](https://github.com/angular/angular-cli/commit/881095eec5cdc80fe79de8fdbe05ba985bf8210a) | fix | enable serving files with bundle-like names | +| [db10af0b3](https://github.com/angular/angular-cli/commit/db10af0b3a775619ac71acdd0258cc58e96e948c) | fix | fix incorrect budget calculation | +| [c822f8f15](https://github.com/angular/angular-cli/commit/c822f8f154b75a8b8e48e32953bee7ec2763fd13) | fix | handle relative URLs when constructing new URLs during server fetch | +| [b43c648b0](https://github.com/angular/angular-cli/commit/b43c648b02c181c1d98cd3293d89ad8b131a3f51) | fix | mitigate JS transformer worker execArgv errors | +| [1f2481a4f](https://github.com/angular/angular-cli/commit/1f2481a4f5155368aa571fc6679e3ef8af0ce56f) | fix | pass `define` option defined in application builder to Vite prebundling | +| [c94f568a4](https://github.com/angular/angular-cli/commit/c94f568a412384bb8e4b66928f41b60220c8b7f4) | fix | warn when `@angular/localize/init` is imported directly | + + + + + +# 19.0.6 (2024-12-18) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------ | +| [db7421231](https://github.com/angular/angular-cli/commit/db7421231c3da7bbbfde72dc35642aaf005fbeca) | fix | jasmine.clock with app builder | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- | +| [5fbc105ed](https://github.com/angular/angular-cli/commit/5fbc105ed0cb76106916660d99fc53d7480dcbc8) | fix | force HTTP/1.1 in dev-server SSR with SSL | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [2f4df6b2b](https://github.com/angular/angular-cli/commit/2f4df6b2be458b3651df49f3bced923e8df4d547) | fix | correctly resolve pre-transform resources in Vite SSR without AppEngine | +| [0789a9e13](https://github.com/angular/angular-cli/commit/0789a9e133fed4edc29b108630b2cf91e157127e) | fix | ensure correct `Location` header for redirects behind a proxy | + + + + + +# 19.0.5 (2024-12-12) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [6c319e44c](https://github.com/angular/angular-cli/commit/6c319e44c707b93e430da93fe815ba839ab18ea1) | fix | fix webpack config transform for karma | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------- | +| [251bd9f22](https://github.com/angular/angular-cli/commit/251bd9f226f73529e824b131fa8d08b77aa00d09) | fix | Fixing auto-csp edge cases where | +| [1047b8635](https://github.com/angular/angular-cli/commit/1047b8635699d55886fff28cbf02d36df224958d) | fix | handle external `@angular/` packages during SSR ([#29094](https://github.com/angular/angular-cli/pull/29094)) | +| [376ee9966](https://github.com/angular/angular-cli/commit/376ee996699a9610984f3d3e36b3331557dbeaca) | fix | provide component HMR update modules to dev-server SSR | +| [5ea9ce376](https://github.com/angular/angular-cli/commit/5ea9ce3760a191d13db08f5ae7448ce089e8eacd) | fix | use consistent path separators for template HMR identifiers | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [b3c6c7eb2](https://github.com/angular/angular-cli/commit/b3c6c7eb2cc796796d99758368706b0b8682ae69) | fix | include `Content-Language` header when locale is set | +| [4203efb90](https://github.com/angular/angular-cli/commit/4203efb90a38fe2f0d45fabab80dc736e8ca2b7b) | fix | disable component bootstrapping during route extraction | + + + + + +# 19.0.4 (2024-12-05) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------ | +| [23667ed4a](https://github.com/angular/angular-cli/commit/23667ed4aa0bedbb591dc0284116402dc42ed95c) | fix | handle windows spec collisions | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [fc41f50b5](https://github.com/angular/angular-cli/commit/fc41f50b53bbffead017b420105eed5bd8573ac1) | fix | show error when Node.js built-ins are used during `ng serve` | +| [14451e275](https://github.com/angular/angular-cli/commit/14451e2754caff2c9800cca17e11ffa452575f09) | perf | reuse TS package.json cache when rebuilding | + + + + + +# 19.0.3 (2024-12-04) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [4e82ca180](https://github.com/angular/angular-cli/commit/4e82ca180b330199b3dffadd9d590c8245dc7785) | fix | correctly select package versions in descending order during `ng add` | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------------------------------------- | +| [28a51cc5e](https://github.com/angular/angular-cli/commit/28a51cc5e4a08f9e9627a1ec160ce462d18b88d2) | fix | add required type to `CanDeactivate` guard ([#29004](https://github.com/angular/angular-cli/pull/29004)) | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [f26e1b462](https://github.com/angular/angular-cli/commit/f26e1b462ab012b0863f0889bcd60f5e07ca6fd2) | fix | add timeout to route extraction | +| [ab4e77c75](https://github.com/angular/angular-cli/commit/ab4e77c75524d42485ac124f4786ab54bc6c404a) | fix | allow .json file replacements with application builds | +| [06690d87e](https://github.com/angular/angular-cli/commit/06690d87eb590853eed6166857c9c1559d38d260) | fix | apply define option to JavaScript from scripts option | +| [775e6f780](https://github.com/angular/angular-cli/commit/775e6f7808e6edb89d29b72ee5bdc6d2b26cb30e) | fix | avoid deploy URL usage on absolute preload links | +| [21f21eda3](https://github.com/angular/angular-cli/commit/21f21eda39c62e284c6cbee0d0ebfe271f605239) | fix | ensure correct handling of `index.output` for SSR | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [75cf47e71](https://github.com/angular/angular-cli/commit/75cf47e71b0584e55750d5350932494f689a7e96) | fix | apply HTML transformation to CSR responses | +| [5880a0230](https://github.com/angular/angular-cli/commit/5880a02306d9f81f030fcdc91fc6aaeb1986e652) | fix | correctly handle serving of prerendered i18n pages | +| [277b8a378](https://github.com/angular/angular-cli/commit/277b8a3786d40cb8477287dcb3ef191ec8939447) | fix | ensure compatibility for `Http2ServerResponse` type | + + + + + +# 19.0.2 (2024-11-25) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------- | +| [2f53e2af5](https://github.com/angular/angular-cli/commit/2f53e2af55794795979232b0f3e95359299e1aee) | fix | skip SSR routing prompt in webcontainer | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- | +| [f9da163f8](https://github.com/angular/angular-cli/commit/f9da163f8852800763844ae89e85eaafe0c37f2b) | fix | minimize reliance on esbuild `inject` to prevent code reordering | +| [c497749e6](https://github.com/angular/angular-cli/commit/c497749e670e916e17a4e7fb0acb1abe26d9bd9a) | fix | prevent errors with parameterized routes when `getPrerenderParams` is undefined | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [c8cd90e0f](https://github.com/angular/angular-cli/commit/c8cd90e0f601a6baa05b84e45bbd37b4bf6049f5) | fix | handle nested redirects not explicitly defined in router config | + + + + + +# 19.0.1 (2024-11-21) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [b63123f20](https://github.com/angular/angular-cli/commit/b63123f20702bd53ea99888b83b4253810ae0a09) | fix | use stylePreprocessorOptions | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [74461da64](https://github.com/angular/angular-cli/commit/74461da6439b70b5348c99682842ae20043d9b61) | fix | ensure accurate content length for server assets | +| [1b4dcedd5](https://github.com/angular/angular-cli/commit/1b4dcedd594b5d9a1701cd8d1e9874742c05e47f) | fix | use `sha256` instead of `sha-256` as hash algorithm name | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------ | +| [8bd2b260e](https://github.com/angular/angular-cli/commit/8bd2b260e2008f1ffc71af0e55b27884c3660c54) | fix | handle baseHref that start with `./` | + + + + + +# 19.0.0 (2024-11-19) + +## Breaking Changes + +### @schematics/angular + +- The app-shell schematic is no longer compatible with Webpack-based builders. + +### @angular-devkit/build-angular + +- The `browserTarget` option has been removed from the DevServer and ExtractI18n builders. `buildTarget` is to be used instead. +- Protractor is no longer supported. + + Protractor was marked end-of-life in August 2023 (see https://protractortest.org/). Projects still relying on Protractor should consider migrating to another E2E testing framework, several support solid migration paths from Protractor. + + - https://angular.dev/tools/cli/end-to-end + - https://blog.angular.dev/the-state-of-end-to-end-testing-with-angular-d175f751cb9c + +### @angular-devkit/core + +- The deprecated `fileBuffer` function is no longer available. Update your code to use `stringToFileBuffer` instead to maintain compatibility. + + **Note:** that this change does not affect application developers. + +### @angular/build + +- The `@angular/localize/init` polyfill will no longer be added automatically to projects. To prevent runtime issues, ensure that this polyfill is manually included in the "polyfills" section of your "angular.json" file if your application relies on Angular localization features. + +### @angular/ssr + +- The `CommonEngine` API now needs to be imported from `@angular/ssr/node`. + + **Before** + + ```ts + import { CommonEngine } from '@angular/ssr'; + ``` + + **After** + + ```ts + import { CommonEngine } from '@angular/ssr/node'; + ``` + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------- | +| [37693c40e](https://github.com/angular/angular-cli/commit/37693c40e3afc4c6dd7c949ea658bdf94146c9d8) | feat | add package manager option to blank schematic | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | +| [a381a3db1](https://github.com/angular/angular-cli/commit/a381a3db187f7b20e5ec8d1e1a1f1bd860426fcd) | feat | add option to export component as default | +| [755f3a07f](https://github.com/angular/angular-cli/commit/755f3a07f5fe485c1ed8c0c6060d6d5c799c085c) | feat | add option to setup new workspace or application as zoneless mode | +| [cfca5442e](https://github.com/angular/angular-cli/commit/cfca5442ec01cc4eff4fe75822eb7ef780ccfef1) | feat | integrate `withEventReplay()` in `provideClientHydration` for new SSR apps | +| [292a4b7c2](https://github.com/angular/angular-cli/commit/292a4b7c2f62828606c42258db524341f4a6391e) | feat | update app-shell and ssr schematics to adopt new Server Rendering API | +| [b1504c3bc](https://github.com/angular/angular-cli/commit/b1504c3bcca4d4c313e5d795ace8b074fb1f8890) | fix | component spec with export default | +| [4b4e000dd](https://github.com/angular/angular-cli/commit/4b4e000dd60bb43df5c8694eb8a0bc0b45d1cf8d) | fix | don't show server routing prompt when using `browser` builder | +| [4e2a5fe15](https://github.com/angular/angular-cli/commit/4e2a5fe155006e7154326319ed39e77e5693d9b3) | fix | enable opt-in for new `@angular/ssr` feature | +| [fcf7443d6](https://github.com/angular/angular-cli/commit/fcf7443d626d1f3e828ebfad464598f7b9ddef12) | fix | explicitly set standalone:false | +| [7992218a9](https://github.com/angular/angular-cli/commit/7992218a9c22ea9469bd3386c7dc1d5efc6e51f9) | fix | remove `declaration` and `sourceMap` from default tsconfig | +| [9e6ab1bf2](https://github.com/angular/angular-cli/commit/9e6ab1bf231b35aceb989337fac55a6136594c5d) | fix | use default import for `express` | + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [201b60e1d](https://github.com/angular/angular-cli/commit/201b60e1dd25b4abb7670e21d103b67d4eda0e14) | feat | handle string key/value pairs, e.g. --define | +| [b847d4460](https://github.com/angular/angular-cli/commit/b847d4460c352604e1935d494efd761915caaa3f) | fix | recommend optional application update migration during v19 update | +| [f249e7e85](https://github.com/angular/angular-cli/commit/f249e7e856bf16e8c5f154fdb8aff36421649a1b) | perf | enable Node.js compile code cache when available | +| [ecc107d83](https://github.com/angular/angular-cli/commit/ecc107d83bfdfd9d5dd1087e264892d60361625c) | perf | enable Node.js compile code cache when available | + +### @angular-devkit/architect + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------- | +| [78f76485f](https://github.com/angular/angular-cli/commit/78f76485fe315ffd0262c1a3732092731235828b) | feat | merge object options from CLI | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------- | +| [0a4ef3026](https://github.com/angular/angular-cli/commit/0a4ef302635e4665ae9881746867dd80ca0d2dc7) | feat | karma-coverage w/ app builder | +| [dcbdca85c](https://github.com/angular/angular-cli/commit/dcbdca85c7fe1a7371b8f6662e0f68e24d56102e) | feat | karma+esbuild+watch | +| [54594b5ab](https://github.com/angular/angular-cli/commit/54594b5abfa4c9301cc369e5dea5f76b71e51ab0) | feat | support karma with esbuild | +| [ea5ae68da](https://github.com/angular/angular-cli/commit/ea5ae68da9e7f2b598bae2ca9ac8be9c20ce7888) | fix | bring back style tags in browser builder | +| [476f94f51](https://github.com/angular/angular-cli/commit/476f94f51a3403d03ceb9f58ffb4a3564cc52e5a) | fix | fix --watch regression in karma | +| [25d928b4f](https://github.com/angular/angular-cli/commit/25d928b4fde00ae8396f6b9cfcd92b5254fc49aa) | fix | fix hanging terminal when `browser-sync` is not installed | +| [2ec877dd0](https://github.com/angular/angular-cli/commit/2ec877dd0dede8f3ee849fe83b4a4427bab96447) | fix | handle basename collisions | +| [ab6e19e1f](https://github.com/angular/angular-cli/commit/ab6e19e1f9a8781334821048522abe86b149c9c3) | fix | handle main field | +| [43e7aae22](https://github.com/angular/angular-cli/commit/43e7aae2284ff15e0282c9d9597c4f31cf1f60a4) | fix | remove double-watch in karma | +| [1e37b5939](https://github.com/angular/angular-cli/commit/1e37b59396a2f815d1671ccecc03ff8441730391) | fix | serve assets | +| [9d7613db9](https://github.com/angular/angular-cli/commit/9d7613db9bf8b397d5896fcdf6c7b0efeaffa5d5) | fix | zone.js/testing + karma + esbuild | +| [e40384e63](https://github.com/angular/angular-cli/commit/e40384e637bc6f92c28f4e572f986ca902938ba2) | refactor | remove deprecated `browserTarget` | +| [62877bdf2](https://github.com/angular/angular-cli/commit/62877bdf2b0449d8c12a167c59d0c24c77467f37) | refactor | remove Protractor builder and schematics | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------ | +| [0d8a1006d](https://github.com/angular/angular-cli/commit/0d8a1006d4629d8af1144065ea237ab30916e66e) | refactor | remove deprecated `fileBuffer` function in favor of `stringToFileBuffer` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------- | +| [b6951f448](https://github.com/angular/angular-cli/commit/b6951f4482418f65e4bd1c15d5f7f051c91d59db) | feat | add `sass` to `stylePreprocessorOptions` in application builder | +| [efb434136](https://github.com/angular/angular-cli/commit/efb434136d8c8df207747ab8fd87b7e2116b7106) | feat | Auto-CSP support as a part of angular.json schema | +| [816e3cb86](https://github.com/angular/angular-cli/commit/816e3cb868961c57a68783601b919370076c41dc) | feat | enable component stylesheet hot replacement by default | +| [3b00fc908](https://github.com/angular/angular-cli/commit/3b00fc908d4f07282e89677928e00665c8578ab5) | feat | introduce `outputMode` option to the application builder | +| [7d883a152](https://github.com/angular/angular-cli/commit/7d883a152e978112245a98f2f737764caa76ec0f) | feat | introduce `ssr.experimentalPlatform` option | +| [c48d6947e](https://github.com/angular/angular-cli/commit/c48d6947ed17eab19822a97492e3686bcf059494) | feat | set development/production condition | +| [f63072668](https://github.com/angular/angular-cli/commit/f63072668e44254da78170445ac2417c7bc1aa18) | feat | utilize `ssr.entry` during prerendering to enable access to local API routes | +| [bbc290133](https://github.com/angular/angular-cli/commit/bbc290133fc93186980ca3c43f221847ba8e858a) | feat | utilize `ssr.entry` in Vite dev-server when available | +| [5a7a2925b](https://github.com/angular/angular-cli/commit/5a7a2925b1f649eabbeb0a75452978cddb3f243d) | fix | add missing redirect in SSR manifest | +| [06e5176c2](https://github.com/angular/angular-cli/commit/06e5176c2d3b27aaeb117374a8ae402c6a4c6319) | fix | add warning when `--prerendering` or `--app-shell` are no-ops | +| [ecaf870b5](https://github.com/angular/angular-cli/commit/ecaf870b5cddd5d43d297f1193eb11b8f73757c0) | fix | always clear dev-server error overlay on non-error result | +| [f8677f6a9](https://github.com/angular/angular-cli/commit/f8677f6a9ba155b04c692814a1bc13f5cc47d94d) | fix | always record component style usage for HMR updates | +| [099e477a8](https://github.com/angular/angular-cli/commit/099e477a8f1bbcf9d0f415dc6fd4743107c967f7) | fix | avoid hashing development external component stylesheets | +| [3602bbb77](https://github.com/angular/angular-cli/commit/3602bbb77b8924e89978427d9115f0b1fd7d46b7) | fix | avoid overwriting inline style bundling additional results | +| [71534aadc](https://github.com/angular/angular-cli/commit/71534aadc403404e2dc9bc12054f32c3ed157db9) | fix | check referenced files against native file paths | +| [fed31e064](https://github.com/angular/angular-cli/commit/fed31e064611894934c86ed36e8b0808029d4143) | fix | correctly use dev-server hmr option to control stylesheet hot replacement | +| [b86bb080e](https://github.com/angular/angular-cli/commit/b86bb080e3a58a3320b2f68fb79edcdc98bfa7e9) | fix | disable dev-server websocket when live reload is disabled | +| [7c50ba9e2](https://github.com/angular/angular-cli/commit/7c50ba9e2faca445c196c69e972ac313547dda54) | fix | ensure `index.csr.html` is always generated when prerendering or SSR are enabled | +| [efb2232df](https://github.com/angular/angular-cli/commit/efb2232df5475699a44d0f76a70e2d7de4a71f5a) | fix | ensure accurate content size in server asset metadata | +| [18a8584ea](https://github.com/angular/angular-cli/commit/18a8584ead439d044760fe2abb4a7f657a0b10e3) | fix | ensure SVG template URLs are considered templates with external stylesheets | +| [7502fee28](https://github.com/angular/angular-cli/commit/7502fee28a057b53e60b97f55b5aff5281019f1b) | fix | Exclude known `--import` from execArgv when spawning workers | +| [2551df533](https://github.com/angular/angular-cli/commit/2551df533d61400c0fda89db77a93378480f5047) | fix | fully disable component style HMR in JIT mode | +| [c41529cc1](https://github.com/angular/angular-cli/commit/c41529cc1d762cf508eccf46c44256df21afe24f) | fix | handle `APP_BASE_HREF` correctly in prerendered routes | +| [87a90afd4](https://github.com/angular/angular-cli/commit/87a90afd4600049b184b32f8f92a0634e25890c0) | fix | incomplete string escaping or encoding | +| [1bb68ba68](https://github.com/angular/angular-cli/commit/1bb68ba6812236a135c1599031bf7e1b7e0d1d79) | fix | move lmdb to optionalDependencies | +| [a995c8ea6](https://github.com/angular/angular-cli/commit/a995c8ea6d17778af031c2f9797e52739ea4dc81) | fix | prevent prerendering of catch-all routes | +| [1654acf0f](https://github.com/angular/angular-cli/commit/1654acf0ff3010b619a22d11f17eec9975d8e2a2) | fix | relax constraints on external stylesheet component id | +| [0d4558ea5](https://github.com/angular/angular-cli/commit/0d4558ea516a4b8716f2442290e05354c502a49e) | fix | set `ngServerMode` during vite prebundling | +| [55d7f01b6](https://github.com/angular/angular-cli/commit/55d7f01b66f4867aad4598574582e8505f201c82) | fix | simplify disabling server features with `--no-server` via command line | +| [cf0228b82](https://github.com/angular/angular-cli/commit/cf0228b828fc43b1b33d48dc0977ff59abb597c2) | fix | skip wildcard routes from being listed as prerendered routes | +| [af52fb49b](https://github.com/angular/angular-cli/commit/af52fb49bdd913af8af9bfbe36be279fce70de39) | fix | synchronize import/export conditions between bundler and TypeScript | +| [6c618d495](https://github.com/angular/angular-cli/commit/6c618d495c54394eb2b87aee36ba5436c06557bd) | fix | update logic to support both internal and external SSR middlewares | +| [bfa8fec9b](https://github.com/angular/angular-cli/commit/bfa8fec9b17d421925a684e2b642dee70165a879) | fix | use named export `reqHandler` for server.ts request handling | +| [c8e1521a2](https://github.com/angular/angular-cli/commit/c8e1521a2bd5b47c811e5d7f9aea7f57e92a4552) | fix | workaround Vite CSS ShadowDOM hot replacement | +| [d6a34034d](https://github.com/angular/angular-cli/commit/d6a34034d7489c09753090642ade4c606cd98ece) | refactor | remove automatic addition of `@angular/localize/init` polyfill and related warnings | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------- | +| [92209dd2e](https://github.com/angular/angular-cli/commit/92209dd2e93af450e3fc657609efe95c6a6b3963) | feat | add `createRequestHandler` and `createNodeRequestHandler `utilities | +| [41fb2ed86](https://github.com/angular/angular-cli/commit/41fb2ed86056306406832317178ca5d94aa110e2) | feat | Add `getHeaders` Method to `AngularAppEngine` and `AngularNodeAppEngine` for handling pages static headers | +| [f346ee8a8](https://github.com/angular/angular-cli/commit/f346ee8a8819bb2eaf0ffb3d5523b00093be09e5) | feat | add `isMainModule` function | +| [d66aaa3ca](https://github.com/angular/angular-cli/commit/d66aaa3ca458e05b535bec7c1dcb98b0e9c5202e) | feat | add server routing configuration API | +| [bca568389](https://github.com/angular/angular-cli/commit/bca56838937f942c5ef902f5c98d018582188e84) | feat | dynamic route resolution using Angular router | +| [30c25bf68](https://github.com/angular/angular-cli/commit/30c25bf6885fefea6094ec1815e066e4c6ada097) | feat | export `AngularAppEngine` as public API | +| [455b5700c](https://github.com/angular/angular-cli/commit/455b5700c29845829235e17efec320e634553108) | feat | expose `writeResponseToNodeResponse` and `createWebRequestFromNodeRequest` in public API | +| [9692a9054](https://github.com/angular/angular-cli/commit/9692a9054c3cdbf151df01279c2d268332b1a032) | feat | improve handling of aborted requests in `AngularServerApp` | +| [576ff604c](https://github.com/angular/angular-cli/commit/576ff604cd739a9f41d588fa093ca2568e46826c) | feat | introduce `AngularNodeAppEngine` API for Node.js integration | +| [3c9697a8c](https://github.com/angular/angular-cli/commit/3c9697a8c34a5e0f3470bde73f11f9f32107f42e) | feat | introduce new hybrid rendering API | +| [4b09887a9](https://github.com/angular/angular-cli/commit/4b09887a9c82838ccb7a6c95d66225c7875e562b) | feat | move `CommonEngine` API to `/node` entry-point | +| [d43180af5](https://github.com/angular/angular-cli/commit/d43180af5f3e7b29387fd06625bd8e37f3ebad95) | fix | add missing peer dependency on `@angular/platform-server` | +| [74b3e2d51](https://github.com/angular/angular-cli/commit/74b3e2d51c6cf605abd05da81dc7b4c3ccd9b3ea) | fix | add validation to prevent use of `provideServerRoutesConfig` in browser context | +| [2640bf7a6](https://github.com/angular/angular-cli/commit/2640bf7a680300acf18cf6502c57a00e0a5bfda9) | fix | correct route extraction and error handling | +| [44077f54e](https://github.com/angular/angular-cli/commit/44077f54e9a95afa5c1f85cf198aaa3412ee08d8) | fix | designate package as side-effect free | +| [df4e1d360](https://github.com/angular/angular-cli/commit/df4e1d3607c2d5bf71d1234fa730e63cd6ab594b) | fix | enable serving of prerendered pages in the App Engine | +| [0793c78cf](https://github.com/angular/angular-cli/commit/0793c78cfcbfc5d55fe6ce2cb53cada684bcb8dc) | fix | ensure wildcard RenderMode is applied when no Angular routes are defined | +| [65b6e75a5](https://github.com/angular/angular-cli/commit/65b6e75a5dca581a57a9ac3d61869fdd20f7dc2e) | fix | export `RESPONSE_INIT`, `REQUEST`, and `REQUEST_CONTEXT` tokens | +| [4ecf63a77](https://github.com/angular/angular-cli/commit/4ecf63a777871bf214bf42fe1738c206bde3201c) | fix | export PrerenderFallback | +| [50df63196](https://github.com/angular/angular-cli/commit/50df631960550049e7d1779fd2c8fbbcf549b8ef) | fix | improve handling of route mismatches between Angular server routes and Angular router | +| [3cf7a5223](https://github.com/angular/angular-cli/commit/3cf7a522318e34daa09f29133e8c3444f154ca0b) | fix | initialize the DI tokens with `null` to avoid requiring them to be set to optional | +| [85df4011b](https://github.com/angular/angular-cli/commit/85df4011ba27254ddb7f22dae550272c9c4406dd) | fix | resolve `bootstrap is not a function` error | +| [e9c9e4995](https://github.com/angular/angular-cli/commit/e9c9e4995e39d9d62d10fe0497e0b98127bc8afa) | fix | resolve circular dependency issue from main.server.js reference in manifest | +| [64c52521d](https://github.com/angular/angular-cli/commit/64c52521d052f850aa7ea1aaadfd8a9fcee9c387) | fix | show error when multiple routes are set with `RenderMode.AppShell` | +| [280ebbda4](https://github.com/angular/angular-cli/commit/280ebbda4c65e19b83448a1bb0de056a2ee5d1c6) | fix | support for HTTP/2 request/response handling | +| [fb05e7f0a](https://github.com/angular/angular-cli/commit/fb05e7f0abd9d68ac03f243c7774260619b8a623) | fix | use wildcard server route configuration on the '/' route when the app router is empty | +| [12ff37adb](https://github.com/angular/angular-cli/commit/12ff37adbed552fc0db97251c30c889ef00e50f3) | perf | cache generated inline CSS for HTML | +| [1d70e3b46](https://github.com/angular/angular-cli/commit/1d70e3b4682806a55d6f7ddacbafcbf615b2a10c) | perf | cache resolved entry-points | +| [f460b91d4](https://github.com/angular/angular-cli/commit/f460b91d46ea5b0413596c4852c80d71d5308910) | perf | integrate ETags for prerendered pages | +| [e52ae7f6f](https://github.com/angular/angular-cli/commit/e52ae7f6f5296a9628cc4a517e82339ac54925bb) | perf | prevent potential stampede in entry-points cache | + + + + + +# 18.2.12 (2024-11-14) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [c3925ed7f](https://github.com/angular/angular-cli/commit/c3925ed7f8e34fd9816cf5a4e8d63c2c45d31d53) | fix | support default options for multiselect list x-prompt | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [c8bee8415](https://github.com/angular/angular-cli/commit/c8bee8415099dfa03d5309183ebbbaab73b2a0eb) | fix | allow .js file replacements in all configuration cases | +| [93f552112](https://github.com/angular/angular-cli/commit/93f552112c2bbd10bc0cee4afcae5b012242636c) | fix | improve URL rebasing for hyphenated Sass namespaced variables | + + + + + +# 18.2.11 (2024-10-30) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [87ec15ba2](https://github.com/angular/angular-cli/commit/87ec15ba266436b7b99b0629beaea3e487434115) | fix | show error message when error stack is undefined | + + + + + +# 18.2.10 (2024-10-23) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- | +| [7b775f4e0](https://github.com/angular/angular-cli/commit/7b775f4e008652777bbe7b788dabed02bcc70cc7) | fix | update `http-proxy-middleware` to `3.0.3` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- | +| [b1e5f51f9](https://github.com/angular/angular-cli/commit/b1e5f51f9111d7da56ebe64cad51936ad659782d) | fix | Address build issue in Node.js LTS versions with prerendering or SSR | + + + + + +# 17.3.11 (2024-10-23) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- | +| [8bad9cee0](https://github.com/angular/angular-cli/commit/8bad9cee08982fffa5ce8244148b491e66191ed8) | fix | update `http-proxy-middleware` to `2.0.7` | + + + + + +# 18.2.9 (2024-10-16) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------- | +| [237f7c5d0](https://github.com/angular/angular-cli/commit/237f7c5d0355e0a90b23156d3aa97f4328c869e7) | fix | update browserslist config to include last 2 Android major versions | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [d749ba6a3](https://github.com/angular/angular-cli/commit/d749ba6a3c3dd7a90317bd9b46e858a842f27696) | fix | allow direct bundling of TSX files with application builder | +| [b91c82d89](https://github.com/angular/angular-cli/commit/b91c82d8997c0009ed4bbf5e9cd9c82cb1f7f755) | fix | avoid race condition in sass importer | + + + + + +# 18.2.8 (2024-10-09) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [b522002ff](https://github.com/angular/angular-cli/commit/b522002fff763cda2ae1c746efcb2638d0099184) | fix | add validation for component and directive class name | +| [dfd2d5c05](https://github.com/angular/angular-cli/commit/dfd2d5c0500777fa5aea91519f6657aed7f3b7d7) | fix | include `index.csr.html` in resources asset group | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [9445916f9](https://github.com/angular/angular-cli/commit/9445916f9b5b9da69623bf86735264d8a5f26fb3) | fix | `Ctrl + C` not terminating dev-server with SSR | +| [9b5cfaa8c](https://github.com/angular/angular-cli/commit/9b5cfaa8ce9d12bf450e7527d479ce7a879ea1b8) | fix | always generate a new hash for optimized chunk | + + + + + +# 18.2.7 (2024-10-02) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [3f98193d6](https://github.com/angular/angular-cli/commit/3f98193d6963464bd04b510c2d045938f1418ff3) | fix | support single quote setting in JetBrains IDEs | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------- | +| [8274184e1](https://github.com/angular/angular-cli/commit/8274184e1c6fa43cc5101018b6fa86fd636a90ba) | fix | add `animate` to valid self-closing elements | +| [2648e811e](https://github.com/angular/angular-cli/commit/2648e811e7c71e8a68d1eb418d7dcdae42ebf9ff) | fix | add few more SVG elements animateMotion, animateTransform, and feBlend etc. to valid self-closing elements | +| [736e126e4](https://github.com/angular/angular-cli/commit/736e126e4836e1c3df32c0ab9ee40e58c16503c0) | fix | separate Vite cache by project | + + + + + +# 18.2.6 (2024-09-25) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [9d0b67124](https://github.com/angular/angular-cli/commit/9d0b67124e4855c5c4a2101b64f8ed86f8624561) | fix | allow missing HTML file request to fallback to index | +| [5fea635b2](https://github.com/angular/angular-cli/commit/5fea635b20b29429e355072c5adc5bf2a597a267) | fix | update rollup to 4.22.4 | + + + + + +# 17.3.10 (2024-09-25) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------- | +| [30489d8fd](https://github.com/angular/angular-cli/commit/30489d8fd1cf738162d95333fe462eea58ba460f) | fix | update vite to 4.1.8 | + + + + + +# 18.2.5 (2024-09-18) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [707431625](https://github.com/angular/angular-cli/commit/7074316257bd736e0d3393368fc93dec9604b49e) | fix | support HTTP HEAD requests for virtual output files | +| [1032b3da1](https://github.com/angular/angular-cli/commit/1032b3da1a0f3aaf63d2fd3cd8c6fd3b0d0b578c) | fix | update vite to `5.4.6` | + + + + + +# 16.2.16 (2024-09-18) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------- | +| [12aca0060](https://github.com/angular/angular-cli/commit/12aca0060492c73cec1bbc231119dde6a4b52607) | fix | update vite to 4.5.5 | + + + + + +# 18.2.4 (2024-09-11) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [765309a2e](https://github.com/angular/angular-cli/commit/765309a2e1bcd3bb07ff87062fc2dc04e4bce16f) | fix | prevent transformation of Node.js internal dependencies by Vite | + + + + + +# 18.2.3 (2024-09-04) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [482076612](https://github.com/angular/angular-cli/commit/482076612cac4b6565fc1b5e89ff9ca207653f87) | fix | update `webpack-dev-middleware` to `7.4.2` | + + + + + +# 18.2.2 (2024-08-29) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [504b00b93](https://github.com/angular/angular-cli/commit/504b00b93b80eec4185838b426c0f6acaa3a148e) | fix | clear context in Karma by default for single run executions | +| [82b76086e](https://github.com/angular/angular-cli/commit/82b76086eb519c224981038dfa55b2ec3cfec0b4) | fix | update webpack to `5.94.0` | + + + + + +# 17.3.9 (2024-08-29) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [e2c5c034d](https://github.com/angular/angular-cli/commit/e2c5c034d96962fe6f358285e376630c71ac9673) | fix | clear context in Karma by default for single run executions | +| [88501f3d5](https://github.com/angular/angular-cli/commit/88501f3d5586f72ee0900b8d351af3d72bdc0dee) | fix | upgrade webpack to `5.94.0` | + + + + + +# 16.2.15 (2024-08-29) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [f596a3d5d](https://github.com/angular/angular-cli/commit/f596a3d5def009b5130440113e3c9b450eb98040) | fix | clear context in Karma by default for single run executions | +| [56fa051bd](https://github.com/angular/angular-cli/commit/56fa051bd92ad47ea089499a488f3566a93375e6) | fix | upgrade webpack to `5.94.0` | + + + + + +# 18.2.1 (2024-08-21) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [05a274a01](https://github.com/angular/angular-cli/commit/05a274a01365c21f69c0412f3455acd14cc6ddc5) | fix | prevent bypassing select/checkbox prompts on validation failure | + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [94e27c88b](https://github.com/angular/angular-cli/commit/94e27c88bb968589bc8b9b5d6536ce6c0ba0b24f) | fix | prevent bypassing select/checkbox prompts on validation failure | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [ddeb2b2b9](https://github.com/angular/angular-cli/commit/ddeb2b2b93eaa9d8b659d17357aa2b7a9dc509ce) | fix | remove outdated browser-esbuild option warning | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------- | +| [83b2699ab](https://github.com/angular/angular-cli/commit/83b2699abbf58a7c90d2339fa4a01d67aa2d2d33) | fix | improve error message when an unhandled exception occurs during prerendering | +| [0be4038a5](https://github.com/angular/angular-cli/commit/0be4038a503626e2e9f44d68fe5599cc6028dd8e) | fix | support reading on-disk files during i18n extraction | + + + + + +# 18.2.0 (2024-08-14) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------- | +| [4da922e4f](https://github.com/angular/angular-cli/commit/4da922e4f4e905a1274e70adca1d875c025b8b46) | feat | use isolatedModules in generated project | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [24aaf1e37](https://github.com/angular/angular-cli/commit/24aaf1e37f49735ce97fe72fced3d53b51d6b631) | feat | support import attribute based loader configuration | + + + + + +# 18.1.4 (2024-08-07) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [f8b092711](https://github.com/angular/angular-cli/commit/f8b092711481a5754ea93bce65d706d261873810) | fix | allow explicitly disabling TypeScript incremental mode | +| [f3a5970fc](https://github.com/angular/angular-cli/commit/f3a5970fca0a196b1ac905306257d65bab3b1325) | fix | lazy load Node.js inspector for dev server | + + + + + +# 18.1.3 (2024-07-31) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------- | +| [a28615d7d](https://github.com/angular/angular-cli/commit/a28615d7dd3f6503f257756058fe182ce6f6b052) | fix | add CSP `nonce` attribute to script tags when inline critical CSS is disabled | +| [747a1447c](https://github.com/angular/angular-cli/commit/747a1447c1855a1bb918e5f55e4ba4182235f88a) | fix | prevent build failures with remote CSS imports when Tailwind is configured | +| [c0933f2c0](https://github.com/angular/angular-cli/commit/c0933f2c0354a13ba3f752f29b24054177697faa) | fix | resolve error with `extract-i18n` builder for libraries | + + + + + +# 18.1.2 (2024-07-24) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [5b9378a3b](https://github.com/angular/angular-cli/commit/5b9378a3be3a1c4f465ba471a120a2edd3a4d2f8) | fix | account for HTML base HREF for dev-server externals | +| [3e4ea77d7](https://github.com/angular/angular-cli/commit/3e4ea77d755edce2c88d55b76860e6e91fb03f3c) | fix | correctly detect comma in Sass URL lexer | +| [d868270f1](https://github.com/angular/angular-cli/commit/d868270f1baf0fd5f2c5677691cc9c4e88b47d8f) | fix | prevent redirection loop | +| [3573ac655](https://github.com/angular/angular-cli/commit/3573ac6555ead2afc34979e284426a0204fff42c) | fix | serve HTML files directly | + + + + + +# 18.1.1 (2024-07-17) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------- | +| [4f6cee272](https://github.com/angular/angular-cli/commit/4f6cee2721b52427624370f3f81f3edc140774e7) | fix | skip undefined files when generating budget stats | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [96dc7e6ed](https://github.com/angular/angular-cli/commit/96dc7e6ed3317e354fae82d1b42b31250e96d89d) | fix | remove Vite "/@id/" prefix for explicit external dependencies | +| [bdef39801](https://github.com/angular/angular-cli/commit/bdef3980160db8c27ae103444a41275351434b78) | fix | resolve only ".wasm" files | + + + + + +# 18.1.0 (2024-07-10) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [6d266c146](https://github.com/angular/angular-cli/commit/6d266c146c9e141396236b93f2bfafcb261fd7aa) | fix | add fallbacks for migration package resolution | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------- | +| [22e05dcb4](https://github.com/angular/angular-cli/commit/22e05dcb4a9ae963997c58fad86125ca51b19a36) | fix | generate new projects with ECMAScript standard class field behavior | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [687a6c7ec](https://github.com/angular/angular-cli/commit/687a6c7eca740a98129196908689a44c181b33a5) | feat | add `--inspect` option to the dev-server | +| [628d87a94](https://github.com/angular/angular-cli/commit/628d87a9474ad2792b69bfbc501a2c5960b27db9) | feat | support WASM/ES Module integration proposal | +| [3e359da8d](https://github.com/angular/angular-cli/commit/3e359da8dfdbfdb99be13f5c52a7e429c106d4ad) | fix | address rxjs undefined issues during SSR prebundling | +| [4ff914a16](https://github.com/angular/angular-cli/commit/4ff914a16525350050c5e8359fb59f9d6f243d27) | fix | allow additional module preloads up to limit | +| [fb8e3c39a](https://github.com/angular/angular-cli/commit/fb8e3c39a8b265003e98c8c6b5a9ec898223249f) | fix | allow top-level await in zoneless applications | +| [83b75af9f](https://github.com/angular/angular-cli/commit/83b75af9f74af0742a6a78cf7531866b7ba434b6) | fix | check inlineSourceMap option with isolated modules optimization | +| [cd97134a6](https://github.com/angular/angular-cli/commit/cd97134a6e1468c6806c2bd753c934ec91bc3927) | fix | normalize paths during module resolution in Vite | +| [13d2100dd](https://github.com/angular/angular-cli/commit/13d2100ddcc670d69f2d7754890b80eae2e7649a) | fix | read WASM file from script location on Node.js | +| [3091956f5](https://github.com/angular/angular-cli/commit/3091956f503754f313dbf98a8de6d21d3d01ebe3) | fix | support import attributes in JavaScript transformer | +| [dd94a831b](https://github.com/angular/angular-cli/commit/dd94a831b4ce1ca55289fca1818aa26195e81dbc) | perf | enable dependency prebundling for server dependencies | +| [3acb77683](https://github.com/angular/angular-cli/commit/3acb7768317bb05a9cd73fa64e081b5ca0326189) | perf | use direct transpilation with isolated modules | + + + + + +# 18.0.7 (2024-07-03) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [67bf90131](https://github.com/angular/angular-cli/commit/67bf9013151c4e6a13c91ecf4afd78c863d9e33f) | fix | make `ng update` to keep newline at the end of package.json | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [9b43ecbd0](https://github.com/angular/angular-cli/commit/9b43ecbd0395027548781a19345fbcd82261d4f4) | fix | reduce the number of max workers to available CPUs minus one | +| [03dad6806](https://github.com/angular/angular-cli/commit/03dad680676c4b2272b65a51dd62d74360e20b78) | fix | rollback terser to `5.29.2` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [fc928f638](https://github.com/angular/angular-cli/commit/fc928f6386061f34f7cd3ef6bb6d25aa4a33a800) | fix | correctly name entry points to match budgets | +| [2d51e8607](https://github.com/angular/angular-cli/commit/2d51e86077c4687224e931f49c82a907f5229ae5) | fix | redirect to path with trailing slash for asset directories | +| [16f1c1e01](https://github.com/angular/angular-cli/commit/16f1c1e010090596b7d7c3911f01728e3feecc4d) | fix | reduce the number of max workers to available CPUs minus one | + + + + + +# 18.0.6 (2024-06-26) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [98a8a8a78](https://github.com/angular/angular-cli/commit/98a8a8a781fd7901f2e1c1d2eb22975ac65f0329) | fix | show JavaScript cache store initialization warning | + + + + + +# 18.0.5 (2024-06-20) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- | +| [5c705e800](https://github.com/angular/angular-cli/commit/5c705e800c17237d82bc9065f22e30b720ddcec0) | fix | update schematics to use RouterModule when --routing flag is present | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [86e031dc7](https://github.com/angular/angular-cli/commit/86e031dc7ef6c736e431caf973aca1233d912060) | fix | use istanbul-lib-instrument directly for karma code coverage | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [bdd168f37](https://github.com/angular/angular-cli/commit/bdd168f37f7757e0c02971e21e90479555a6e703) | fix | add CSP nonce to script with src tags | +| [405c14809](https://github.com/angular/angular-cli/commit/405c1480917d50c677be178c817b845f30cc8cce) | fix | automatically resolve `.mjs` files when using Vite | +| [7360a346e](https://github.com/angular/angular-cli/commit/7360a346ed1b329c0620301ce0e0464d5569a42f) | fix | use Node.js available parallelism for default worker count | + + + + + +# 18.0.4 (2024-06-13) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------- | +| [791ef809d](https://github.com/angular/angular-cli/commit/791ef809d8dfec8fde844e916973a05b2eb5c9d9) | fix | do not reference sourcemaps in web workers and global stylesheet bundles when hidden setting is enabled | +| [20fc6ca05](https://github.com/angular/angular-cli/commit/20fc6ca057e5190155474e7377bf9f22aab597dd) | fix | generate module preloads next to script elements in index HTML | +| [3a1bf5c8a](https://github.com/angular/angular-cli/commit/3a1bf5c8a52d6ec1eb337f0937bf073de2ea0b62) | fix | Initiate PostCSS only once | +| [78c611754](https://github.com/angular/angular-cli/commit/78c6117544afa1aa69ef5485f1c3b77b1207f6f1) | fix | issue warning when auto adding `@angular/localize/init` | + + + + + +# 18.0.3 (2024-06-05) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------- | +| [b709d2a24](https://github.com/angular/angular-cli/commit/b709d2a243926f1ab01e8c8a78d68fc57ab4cab3) | fix | add `schema.json` options to parsed command, also when a version is passed to `ng add @` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [43a2a7d13](https://github.com/angular/angular-cli/commit/43a2a7d137328c1f6f865b76585a92f4e5058b13) | fix | avoid escaping rebased Sass URL values | +| [9acb5c7ca](https://github.com/angular/angular-cli/commit/9acb5c7ca8e6d14379e39f56d2498c0276214210) | fix | disable JS transformer persistent cache on web containers | +| [346df4909](https://github.com/angular/angular-cli/commit/346df490976e39d791db5ecfa14eab6a5ad8d99d) | fix | improve Sass rebaser ident token detection | +| [6526a5f59](https://github.com/angular/angular-cli/commit/6526a5f590fbc7f26b9e613af3b5c497e30603b5) | fix | watch all related files during a Sass error | + + + + + +# 18.0.2 (2024-05-29) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [9967c04b8](https://github.com/angular/angular-cli/commit/9967c04b86c6928509c80af7144b342616e9681b) | fix | check both application builder packages in SSR schematic | +| [92b48ab14](https://github.com/angular/angular-cli/commit/92b48ab144fbe5b8f89d371b0a8fa94d0d17b72c) | fix | set builders `assets` option correctly for new applications | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [3bb06c37d](https://github.com/angular/angular-cli/commit/3bb06c37dc20d7af358f007b9928de71f39545d2) | fix | disable Worker wait loop for Sass compilations in web containers | +| [c4cf35923](https://github.com/angular/angular-cli/commit/c4cf359233e1044864539383912b9ba0432e149d) | fix | print Sass `@warn` location | +| [352879804](https://github.com/angular/angular-cli/commit/3528798042a232779478bf82b4d4f6521fab4c30) | fix | support valid self-closing MathML tags in HTML index file | +| [476f3084a](https://github.com/angular/angular-cli/commit/476f3084aff333a45c4937950abdba65cd420eba) | fix | support valid self-closing SVG tags in HTML index file | + +### @angular/pwa + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [acbffd236](https://github.com/angular/angular-cli/commit/acbffd2368d3c979b26a4541d3f44387fdba0651) | fix | set manifest `icons` location to match `assets` builder option | + + + + + +# 18.0.1 (2024-05-23) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [01842f515](https://github.com/angular/angular-cli/commit/01842f5154fe0ec41226d1dd28e099bf57f3d2c9) | fix | use angular.dev in readme | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [7d253e9cd](https://github.com/angular/angular-cli/commit/7d253e9cd0bb6df829fd4229465f4334d5c134bb) | fix | avoid rebasing URLs with function calls | +| [6b6a76a99](https://github.com/angular/angular-cli/commit/6b6a76a998980392d78e1cabc5e5fe4af0ced01c) | fix | disable persistent disk caching inside webcontainers by default | +| [ba70a50b6](https://github.com/angular/angular-cli/commit/ba70a50b6bc45a6b07ff24feed3b36915294063b) | fix | handle esbuild-browser `polyfills` option as `string` during `ng serve` | +| [706423aca](https://github.com/angular/angular-cli/commit/706423acad2c431c4125166d078dbad804719d95) | fix | only import persistent cache store with active caching | + + + + + +# 18.0.0 (2024-05-22) + +## Breaking Changes + +### @angular/cli + +- The `ng doc` command has been removed without a replacement. To perform searches, please visit www.angular.dev +- Node.js support for versions <18.19.1 and <20.11.1 has been removed. + +### @angular-devkit/build-angular + +- By default, the index.html file is no longer emitted in the browser directory when using the application builder with SSR. Instead, an index.csr.html file is emitted. This change is implemented because in many cases server and cloud providers incorrectly treat the index.html file as a statically generated page. If you still require the old behavior, you can use the `index` option to specify the `output` file name. + + ```json + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/my-app", + "index": { + "input": "src/index.html", + "output": "index.html" + } + } + } + } + ``` + +- The support for the legacy Sass build pipeline, previously accessible via `NG_BUILD_LEGACY_SASS` when utilizing webpack-based builders, has been removed. + +## Deprecations + +### @angular-devkit/schematics + +- `NodePackageLinkTask` in `@angular-devkit/schematics`. A custom task should be created instead. + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------- | +| [ac3019570](https://github.com/angular/angular-cli/commit/ac301957093d0689c98f7debe98fbb2546c9b442) | feat | add `ng dev` alias to `ng serve` | +| [4087728c3](https://github.com/angular/angular-cli/commit/4087728c3e6350d85d653e9d053249ff77e639e6) | feat | support for Node.js v22 | +| [41ab6c8c3](https://github.com/angular/angular-cli/commit/41ab6c8c3486d7cf7c41c18ae3b603376f647605) | fix | add `--version` option | +| [df4dde95d](https://github.com/angular/angular-cli/commit/df4dde95daa12d5b08b3c4e937f4b4048d645254) | fix | add `@angular/build` package to update group list | +| [1039f6d79](https://github.com/angular/angular-cli/commit/1039f6d7997523dd4657c5c2a06631e6075b7bc0) | fix | change update guide link to angular.dev | +| [f4670fcb1](https://github.com/angular/angular-cli/commit/f4670fcb1af20a53501b557fc0e6126afce766d5) | fix | eliminate prompts during `ng version` command | +| [a99ec6a54](https://github.com/angular/angular-cli/commit/a99ec6a5453fb732500ef7abff67f76511a74da3) | fix | keep cli package first in update package group metadata | +| [dd786d495](https://github.com/angular/angular-cli/commit/dd786d495ce6e7d759b0b225b2efe25fb5727d08) | fix | only add --version option on default command | +| [03eee0545](https://github.com/angular/angular-cli/commit/03eee0545095ff958ac86cb5dfad44692ef018ae) | refactor | remove `ng doc` command | +| [c7b208555](https://github.com/angular/angular-cli/commit/c7b208555e34cc5ebf9cf2d335d257e72297cae9) | refactor | remove support for Node.js versions <18.19.1 and <20.11.1 | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- | +| [b2ac5fac7](https://github.com/angular/angular-cli/commit/b2ac5fac7d66ccd027f766565fa17c6a3bb18e44) | feat | allow application migration to use new build package in projects where possible | +| [6530aa11b](https://github.com/angular/angular-cli/commit/6530aa11bed5ef67d611e8aed268bd20345cf0e6) | feat | replace `assets` with `public` directory | +| [725883713](https://github.com/angular/angular-cli/commit/72588371385bebeea1003dff4d1d0a2ca9854321) | feat | use eventCoalescing option by default (standalone bootstrap) | +| [508d97da7](https://github.com/angular/angular-cli/commit/508d97da76b5359bc8029888ff0e9cfc59a6139c) | feat | use ngZoneEventCoalescing option by default (module bootstrap) | +| [f452589e2](https://github.com/angular/angular-cli/commit/f452589e2c921448b76a138a5f34ba92ad05e297) | feat | use TypeScript bundler module resolution for new projects | +| [95a4d6ee5](https://github.com/angular/angular-cli/commit/95a4d6ee56d80dce012cf2306422bb7fd8e0e32d) | fix | add less dependency in application migration if needed | +| [c46aa084f](https://github.com/angular/angular-cli/commit/c46aa084f53be7ebdb8cc450bd81907222d00275) | fix | add postcss dependency in application migration if needed | +| [157329384](https://github.com/angular/angular-cli/commit/157329384809d723c428a043712a331493826748) | fix | add spaces around eventCoalescing option | +| [23cc337aa](https://github.com/angular/angular-cli/commit/23cc337aa34c919e344ab001f5efbb8fe9ce3c7c) | fix | keep deployUrl option when migrating to application builder | + +### @angular-devkit/architect + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [ddd08efef](https://github.com/angular/angular-cli/commit/ddd08efefecfe9b74db6a866a1bed0216380a28a) | fix | resolve builder aliases from containing package | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------- | +| [53c319aaa](https://github.com/angular/angular-cli/commit/53c319aaa95049b8558df80e57fa0a6318003121) | feat | add support for the `poll` option in the library builder | +| [83d1d233a](https://github.com/angular/angular-cli/commit/83d1d233a2eded71fcdd5fec4b1a90bdd4dbf132) | feat | enhance Sass rebasing importer for resources URL defined in variables and handling of external paths | +| [d51cb598a](https://github.com/angular/angular-cli/commit/d51cb598a74aba313aee212656de506004a041e6) | feat | inject event-dispatch in SSR HTML page | +| [0b03829bc](https://github.com/angular/angular-cli/commit/0b03829bcefea5c250c6a9ff880a737fcc351b2e) | feat | move i18n extraction for application builder to new build system package | +| [4ffe07aa2](https://github.com/angular/angular-cli/commit/4ffe07aa24a0fc9ff48461e9c3664d96e92317cf) | feat | move Vite-based dev-server for application builder to new build system package | +| [d1c632af9](https://github.com/angular/angular-cli/commit/d1c632af9a98d4e8975f198cf205194e2ebff209) | feat | support native async/await when app is zoneless | +| [37fc7f0cc](https://github.com/angular/angular-cli/commit/37fc7f0ccf3b8e6f31a0c5b2eaf4aee52f439472) | fix | disable Vite prebundling when script optimizations are enabled | +| [2acf95a94](https://github.com/angular/angular-cli/commit/2acf95a94993e51876d4004d2c3bc0a04be0a419) | fix | do not generate an `index.html` file in the browser directory when using SSR. | +| [8a54875cb](https://github.com/angular/angular-cli/commit/8a54875cbb654f95d5213b2d84190bd3814d6810) | fix | handle wrapping of class expressions emitted by esbuild | +| [97973059e](https://github.com/angular/angular-cli/commit/97973059ec56a573629f7a367757773a3cfabe17) | refactor | remove Sass legacy implementation | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ------------------------------- | +| [797584583](https://github.com/angular/angular-cli/commit/797584583138c9223bf238ae8f352e77575bd25a) | refactor | deprecate `NodePackageLinkTask` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------ | +| [810d213e1](https://github.com/angular/angular-cli/commit/810d213e1813dd01620173f5f999dca7bccf8ea1) | feat | introduce new official build system package | +| [b7a0792b3](https://github.com/angular/angular-cli/commit/b7a0792b3286fc98d1343f55b5df89ddf13e36bc) | fix | add a maximum rendering timeout for SSG | +| [411115303](https://github.com/angular/angular-cli/commit/41111530349db1ac199c3ac1d4eccbde8b023123) | fix | add console note about development server raw file size | +| [921fa7cf4](https://github.com/angular/angular-cli/commit/921fa7cf4adc69d3cb6ec7dd5c8d7cace33a502e) | fix | add missing `ansi-colors` and `picomatch` dependencies | +| [791cf75af](https://github.com/angular/angular-cli/commit/791cf75afb0b3b5892c41296bc4049a2c10926e8) | fix | check both potential build packages in Angular version check | +| [4d7cd5e3e](https://github.com/angular/angular-cli/commit/4d7cd5e3ed303c53b2cc63720b9a577e2f46f170) | fix | correctly wrap class expressions with static properties or blocks emitted by esbuild | +| [57f448a0f](https://github.com/angular/angular-cli/commit/57f448a0f70c76c1a0ebbe941f82eec1d698e7d4) | fix | decode URL pathname decoding during SSG fetch | +| [940e382db](https://github.com/angular/angular-cli/commit/940e382db27474dba6479f57e4ffefee04cfca66) | fix | disable Vite prebundling when script optimizations are enabled | +| [70dbc7a6e](https://github.com/angular/angular-cli/commit/70dbc7a6e9a7f6d55aeb4e10e8e686b186e6cdf3) | fix | emit error for invalid self-closing element in index HTML | +| [44b401747](https://github.com/angular/angular-cli/commit/44b401747f78bab208ce863f9c08e7a12f01fe27) | fix | ensure input index HTML file triggers rebuilds when changed | +| [dff4deaeb](https://github.com/angular/angular-cli/commit/dff4deaeb366d0ff734ae02abdbaa1fcdcd901aa) | fix | ensure recreated files are watched | +| [17931166d](https://github.com/angular/angular-cli/commit/17931166d83a4b18d2f4eb81f8a445b2365c71aa) | fix | format sizes using decimal byte units consistently | +| [2085365e0](https://github.com/angular/angular-cli/commit/2085365e04c9b08dbf2024036b93609046f2f458) | fix | only generate shallow preload links for initial files | +| [33cd47c85](https://github.com/angular/angular-cli/commit/33cd47c85ea12df57ec7b244beccfa299c927765) | fix | properly configure headers for media resources and HTML page | +| [d10fece2c](https://github.com/angular/angular-cli/commit/d10fece2c17183e18d04733dec22459ced1cc1c8) | fix | properly rebase Sass url() values with leading interpolations | +| [3f2963835](https://github.com/angular/angular-cli/commit/3f2963835759fa3eed1faf64a7b87d5dcf8a6fa3) | perf | add persistent caching of JavaScript transformations | +| [a15eb7d1c](https://github.com/angular/angular-cli/commit/a15eb7d1c6a26f5d94da5566f8b4ac1810ea1361) | perf | improve rebuild time for file loader usage with prebundling | + + + + + +# 17.3.8 (2024-05-22) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [3ada6eb52](https://github.com/angular/angular-cli/commit/3ada6eb5256631ca3a951525fc9814ad0447a41f) | fix | clarify optional migration instructions during ng update | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------- | +| [4b6ba8df1](https://github.com/angular/angular-cli/commit/4b6ba8df1ab8f4801fba7ddc38812417e274d960) | fix | `SchematicTestRunner.runExternalSchematic` fails with "The encoded data was not valid for encoding utf-8" | + + + + + +# 17.3.7 (2024-05-08) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------- | +| [998c72036](https://github.com/angular/angular-cli/commit/998c720363087f3f0b1748d3f875e5b536a3119d) | fix | decode URL pathname decoding during SSG fetch | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------- | +| [1ab1c6c9e](https://github.com/angular/angular-cli/commit/1ab1c6c9e10ce938402355afed4602b76ac08a0e) | fix | use web standard error check for Deno support | + + + + + +# 17.3.6 (2024-04-25) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [dcec59799](https://github.com/angular/angular-cli/commit/dcec59799faac66bf25043984c11944479efcf4d) | fix | properly configure headers for media resources and HTML page | + + + + + +# 17.3.5 (2024-04-17) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [6191d06ca](https://github.com/angular/angular-cli/commit/6191d06ca735a8fd29f048f319f912076abec698) | fix | address `Unable to deserialize cloned data` issue with Yarn PnP | +| [0335d6a5d](https://github.com/angular/angular-cli/commit/0335d6a5df1c0b0706673e6152e3bf5eb65d166a) | fix | remove `type="text/css"` from `style` tag | + + + + + +# 17.3.4 (2024-04-11) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [1128bdd64](https://github.com/angular/angular-cli/commit/1128bdd642c3e60c67485970e5cd354b2fde0f98) | fix | ensure esbuild-based builders exclusively produce ESM output | + + + + + +# 16.2.14 (2024-04-11) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------- | +| [1068c3c73](https://github.com/angular/angular-cli/commit/1068c3c733a7c52e7876d43454d0ff590c99b61b) | fix | update vite to `4.5.3` | + + + + + +# 17.3.3 (2024-04-02) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [a673baf5c](https://github.com/angular/angular-cli/commit/a673baf5ce385415ded23641a2dc5cdcae8f3f5c) | fix | Revert "fix(@schematics/angular): rename SSR port env variable" | + + + + + +# 17.3.2 (2024-03-25) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [935f931ee](https://github.com/angular/angular-cli/commit/935f931eea8ddd1cb86b2275b8e384bf51e9153e) | fix | rename SSR port env variable | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------ | +| [c9d436000](https://github.com/angular/angular-cli/commit/c9d4360000e6134b936781be3b0d5cf1871d44d7) | fix | `Internal server error: Invalid URL` when using a non localhost IP | +| [59fba38ec](https://github.com/angular/angular-cli/commit/59fba38ec6181c8d9c7a0636fb514c4b25aaf0cd) | fix | ensure proper resolution of linked SCSS files | +| [27dd8f208](https://github.com/angular/angular-cli/commit/27dd8f208911dbb2eda6d64efd6d1ce8c463ce35) | fix | service-worker references non-existent named index output | +| [c12907d92](https://github.com/angular/angular-cli/commit/c12907d92f8a2268541fd3bf28857dbb216ec7c9) | fix | update `webpack-dev-middleware` to `6.1.2` | + + + + + +# 16.2.13 (2024-03-25) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [5ad507e3d](https://github.com/angular/angular-cli/commit/5ad507e3d4cb27fb275d255018b9b6e735835711) | fix | `update webpack-dev-middleware` to `6.1.2` | + + + + + +# 15.2.11 (2024-03-25) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [c6feb0bb0](https://github.com/angular/angular-cli/commit/c6feb0bb0247a1cf17e17325b8c42d0d6a7d1451) | fix | `update webpack-dev-middleware` to `6.1.2` | + + + + + +# 17.3.1 (2024-03-20) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [198ca9afc](https://github.com/angular/angular-cli/commit/198ca9afcc9a043d4329c2f4032f0b9cefa11a2e) | fix | improve Sass format clarity for application style option | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------- | +| [2809971a5](https://github.com/angular/angular-cli/commit/2809971a57966cf79965c84a933f70709334c16b) | fix | only generate `server` directory when SSR is enabled | +| [3f601a14e](https://github.com/angular/angular-cli/commit/3f601a14e70540f37ef6c6559a5cd50bb6b453d7) | fix | typo in allowedHosts warning for unsupported vite options | +| [79c44adac](https://github.com/angular/angular-cli/commit/79c44adac4184408cbd1dc07989796f155cfc70e) | perf | avoid transforming empty component stylesheets | +| [cc3167f30](https://github.com/angular/angular-cli/commit/cc3167f3012aa621a7fc9277a9c8a82601f7d914) | perf | reduce build times for apps with a large number of components when utilizing esbuild-based builders | + + + + + +# 17.3.0 (2024-03-13) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [5ab71fc92](https://github.com/angular/angular-cli/commit/5ab71fc92ba26f6255e5a5c00e374709ff58d19d) | feat | update CSS/Sass import/use specifiers in application migration | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------- | +| [9ca3a5450](https://github.com/angular/angular-cli/commit/9ca3a54503574674eb107d4d2b507be7ecd727ee) | feat | add `deployUrl` to application builder | +| [3821344da](https://github.com/angular/angular-cli/commit/3821344da663ded52b773752abc805ed04e28f3c) | fix | ensure proper display of build logs in the presence of warnings or errors | +| [de2d05049](https://github.com/angular/angular-cli/commit/de2d050498e16efa75459f526b9973c9e1d67050) | fix | provide better error message when server option is required but missing | + + + + + +# 17.2.3 (2024-03-06) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------- | +| [7cc8261fd](https://github.com/angular/angular-cli/commit/7cc8261fd2eb0ef1665faebec43cba447a64fd33) | fix | avoid implicit CSS file extensions when resolving | +| [259ec72d5](https://github.com/angular/angular-cli/commit/259ec72d521b3a660c54ec33aaf8bf7c838281e7) | fix | avoid marking component styles as media with no output media directory | +| [faffdfdce](https://github.com/angular/angular-cli/commit/faffdfdcebb3f71f7ef64b02114561985c592add) | fix | disable `deployUrl` when using vite dev-server | + + + + + +# 17.2.2 (2024-02-28) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [3394d3cf1](https://github.com/angular/angular-cli/commit/3394d3cf10996a93777edfb28d83cf4eb85ae40b) | fix | ensure all related stylesheets are rebuilt when an import changes | + + + + + +# 17.2.1 (2024-02-22) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [9e7c47b59](https://github.com/angular/angular-cli/commit/9e7c47b5945b368a6fd5e2544674d5a3afd63d65) | fix | allow `mts` and `cts` file replacement | +| [f2a2e9287](https://github.com/angular/angular-cli/commit/f2a2e92877298a30bc1042772be043d5db9ac729) | fix | provide Vite client code source map when loading | + + + + + +# 17.2.0 (2024-02-14) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------ | +| [b3e206741](https://github.com/angular/angular-cli/commit/b3e206741c5b59b8563b7c60a1f66c49802549e7) | feat | add support to `bun` package manager | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------ | +| [03e1aa790](https://github.com/angular/angular-cli/commit/03e1aa7904acfe9d12eaf3717d1b136159893a76) | feat | add support to `bun` package manager | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------ | +| [7f57123fd](https://github.com/angular/angular-cli/commit/7f57123fd40b345d7880cb9e2eccd4757c0fb6ee) | feat | add define build option to application builder | +| [f4f535653](https://github.com/angular/angular-cli/commit/f4f535653a34c2a7c37c51c98680b6b1766c6d0d) | feat | add JSON build logs when using the application builder | +| [b59f663e5](https://github.com/angular/angular-cli/commit/b59f663e5715e009b05bf560637c1bca3b112784) | feat | allow control of Vite-based development server prebundling | +| [8f47f1e96](https://github.com/angular/angular-cli/commit/8f47f1e965b25f3d976eda701ae2e7b7e8cccfa3) | feat | provide default and abbreviated build target support for dev-server and extract-i18n | +| [7a12074dc](https://github.com/angular/angular-cli/commit/7a12074dc940f1aa8f5347071324fa0d2fd1300b) | feat | provide option to allow automatically cleaning the terminal screen during rebuilds | +| [7c522aa87](https://github.com/angular/angular-cli/commit/7c522aa8742cd936bb0dfd30773d88f3ef92d488) | feat | support using custom postcss configuration with application builder | +| [476a68daa](https://github.com/angular/angular-cli/commit/476a68daa9746d29d3f74f68307d982d1d66f94c) | fix | add output location in build stats | +| [5e6f1a9f4](https://github.com/angular/angular-cli/commit/5e6f1a9f4362e9b12db64c7c2e609a346b17963d) | fix | avoid preloading server chunks | +| [41ea985f9](https://github.com/angular/angular-cli/commit/41ea985f9317b11cfa6627a2d3f6b34ff4dbc134) | fix | display server bundles in build stats | +| [d493609d3](https://github.com/angular/angular-cli/commit/d493609d30e1f148a7efb72bd64227521a326fbb) | fix | downgrade copy-webpack-plugin to workaround Node.js support issue | +| [8d5af1d5c](https://github.com/angular/angular-cli/commit/8d5af1d5c78ce9aa996f6ba138b99d0bb5005d46) | fix | ensure correct `.html` served with Vite dev-server | +| [944cbcdb1](https://github.com/angular/angular-cli/commit/944cbcdb1af62855febc931fce92debf28a3b2a5) | fix | limit the number of lazy chunks visible in the stats table | +| [905e13633](https://github.com/angular/angular-cli/commit/905e13633071b1db25621ae9f2762a48fe010df1) | fix | support string as plugin option in custom postcss plugin config | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------- | +| [da1c38c48](https://github.com/angular/angular-cli/commit/da1c38c486b07d5a1b2933f23f83c6231b512e0f) | fix | add `bun` to known package managers | + +### @angular/create + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------ | +| [600498f2c](https://github.com/angular/angular-cli/commit/600498f2cd3e3251e7e6e3dd3505c5e943b2986a) | feat | add support to `bun` package manager | + + + + + +# 17.1.4 (2024-02-14) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [6d2168db9](https://github.com/angular/angular-cli/commit/6d2168db92fcba1ebf82498fed85cd2b596547d3) | fix | prevent BOM errors in `package.json` during `ng update` | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [bf42d6df2](https://github.com/angular/angular-cli/commit/bf42d6df2f5eda45bec80bb60315152c03f4a3dc) | fix | bypass Vite prebundling for absolute URL imports | + + + + + +# 17.1.3 (2024-02-08) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [3de3aa170](https://github.com/angular/angular-cli/commit/3de3aa170f02352fe2adf61beea221b356a40843) | fix | allow `./` baseHref when using vite based server | +| [17f47a3c9](https://github.com/angular/angular-cli/commit/17f47a3c94b434a73b9fc698872ef6230f61c781) | fix | ensure WebWorker main entry is used in output code | + + + + + +# 17.1.2 (2024-01-31) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [6815f13e3](https://github.com/angular/angular-cli/commit/6815f13e3c87eff773aa914858293c75e4fae7d2) | fix | add `required` modules as externals imports | +| [a0e306098](https://github.com/angular/angular-cli/commit/a0e306098147cf5fb7b51264c18860767fdf6316) | fix | correctly handle glob negation in proxy config when using vite | +| [235c8403a](https://github.com/angular/angular-cli/commit/235c8403a5bf8a2032da72a504e8cee441dd2d82) | fix | handle regular expressions in proxy config when using Vite | +| [5332e5b2e](https://github.com/angular/angular-cli/commit/5332e5b2ea0c9757f717e386fb162392ef2327a4) | fix | resolve absolute `output-path` when using esbuild based builders | +| [3deb0d4a1](https://github.com/angular/angular-cli/commit/3deb0d4a102fb8d8fae7617b81f62706371e03f5) | fix | return 404 for assets that are not found | + + + + + +# 17.1.1 (2024-01-24) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- | +| [8ebb754c2](https://github.com/angular/angular-cli/commit/8ebb754c2e865ffd2c38f61d50a5f4be225a0fe5) | fix | update regex to validate the project-name | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [35ebf1efd](https://github.com/angular/angular-cli/commit/35ebf1efdfa438ea713020b847826621bba0ebfc) | fix | retain trailing comma when adding providers to app config | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------- | +| [88de1da92](https://github.com/angular/angular-cli/commit/88de1da92919834f620a31d8a3e6a4e2ad1e2f07) | fix | `ENOENT: no such file or directory` on Windows during component rebuild | +| [4e2586aeb](https://github.com/angular/angular-cli/commit/4e2586aeb8ec11cf951f30bbfca6422f13cfd5cc) | fix | allow package file loader option with Vite prebundling | +| [aca1cfcda](https://github.com/angular/angular-cli/commit/aca1cfcda520d9a68bc01833453c81f38c133d37) | fix | do not add internal CSS resources files in watch | +| [53258f617](https://github.com/angular/angular-cli/commit/53258f617cf6c9068e069122029ff91c62d2109e) | fix | handle load event for multiple stylesheets and CSP nonces | +| [412fe6ec6](https://github.com/angular/angular-cli/commit/412fe6ec69bfcbb1e9fb09ccbb10a086b5166689) | fix | pre-transform error when using vite with SSR | +| [45dea6f44](https://github.com/angular/angular-cli/commit/45dea6f44cb27431e4767ce16df3e84c5b6d8f9c) | fix | provide actionable error message when server bundle is missing default export | +| [4e2b23f03](https://github.com/angular/angular-cli/commit/4e2b23f0321f3ec6edfd3e20e9bf95d799de5e7f) | fix | update dependency vite to v5.0.12 | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [02d9d84c5](https://github.com/angular/angular-cli/commit/02d9d84c5da3def7e6b307b115e77233cfcf8d4b) | fix | handle load event for multiple stylesheets and CSP nonces | + + + + + +# 16.2.12 (2024-01-24) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------- | +| [5fad40162](https://github.com/angular/angular-cli/commit/5fad401628f7ddbc412d7e761a4300724f078bde) | fix | update dependency vite to v4.5.2 | + + + + + +# 17.1.0 (2024-01-17) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------- | +| [b513d89b7](https://github.com/angular/angular-cli/commit/b513d89b77dd50891a5f02ec59d1a2bffa0d36db) | feat | add optional migration to use application builder | +| [a708dccff](https://github.com/angular/angular-cli/commit/a708dccff34f62b625332555005bbd8f41380ec2) | feat | update SSR and application builder migration schematics to work with new `outputPath` | +| [4469e481f](https://github.com/angular/angular-cli/commit/4469e481fc4f74574fdd028513b57ba2300c3b34) | fix | do not trigger NPM install when using `---skip-install` and `--ssr` | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- | +| [e0b274b8f](https://github.com/angular/angular-cli/commit/e0b274b8ff4d164061ca7b60248bb85ceee8f65d) | feat | add option to retain CSS special comments in global styles | +| [204794c4f](https://github.com/angular/angular-cli/commit/204794c4f8e87882af974144fff642762930b4d3) | feat | add support for `--no-browsers` in karma builder | +| [4784155bd](https://github.com/angular/angular-cli/commit/4784155bd62cfac9b29327167093e70c9c6bee41) | feat | add wildcard option for `allowedCommonJsDependencies` | +| [3b93df42d](https://github.com/angular/angular-cli/commit/3b93df42daf9eda9215ea65d8ed0efd1ef203a09) | feat | allow configuring loaders for custom file extensions in application builder | +| [cc246d50e](https://github.com/angular/angular-cli/commit/cc246d50ea8d92289c8be8dc58b376358a899ad6) | feat | allow customization of output locations | +| [15a669c1e](https://github.com/angular/angular-cli/commit/15a669c1efdc8ac18507232d6cb29794c82b94cc) | feat | allowing control of index HTML initial preload generation | +| [47a064b14](https://github.com/angular/angular-cli/commit/47a064b146d06ee7498e3aacb2f17a6283be4504) | feat | emit external sourcemaps for component styles | +| [68dae539a](https://github.com/angular/angular-cli/commit/68dae539adfa12d6088f96ac5c9f224d9bb52e17) | feat | initial experimental implementation of `@web/test-runner` builder | +| [f6e67df1c](https://github.com/angular/angular-cli/commit/f6e67df1c4f286fb1fe195b75cdaab4339ad7604) | feat | inline Google and Adobe fonts located in stylesheets | +| [364a16b7a](https://github.com/angular/angular-cli/commit/364a16b7a6d903cb176f7db627fec126b8aa05f9) | feat | move `browser-sync` as optional dependency | +| [ccba849e4](https://github.com/angular/angular-cli/commit/ccba849e48287805bd8253a03f88d5f44b2b23ae) | feat | support keyboard command shortcuts in application dev server | +| [329d80075](https://github.com/angular/angular-cli/commit/329d80075bc788de0c8e757fbd8cd69120fbec99) | fix | alllow `OPTIONS` requests to be proxied when using `vite` | +| [49ed9a26c](https://github.com/angular/angular-cli/commit/49ed9a26cb87ae629d7d4167277f7e5c4ee066f7) | fix | emit error when using prerender and app-shell builders with application builder | +| [6473b0160](https://github.com/angular/angular-cli/commit/6473b01603b55d265489840cbf32697ad663aeeb) | fix | ensure all configured assets can be served by dev server | +| [874e576b5](https://github.com/angular/angular-cli/commit/874e576b523ba675f85011388e4ce3fcc38992fa) | fix | filter explicit external dependencies for Vite prebundling | +| [2a02b1320](https://github.com/angular/angular-cli/commit/2a02b1320449e0562041bbba86e42048665402e5) | fix | fix normalization of the application builder extensions | +| [9906ab7b4](https://github.com/angular/angular-cli/commit/9906ab7b4714e1fca040f875dd91f0279f688abe) | fix | normalize asset source locations in Vite-based development server | +| [ceffafe1a](https://github.com/angular/angular-cli/commit/ceffafe1a3c8cad469b718e466e771e1d396ab14) | fix | provide better error messages for failed file reads | +| [6d7fdb952](https://github.com/angular/angular-cli/commit/6d7fdb952d49dda1301af229af138d834161c2f9) | fix | show diagnostic messages after build stats | +| [4e1f0e44d](https://github.com/angular/angular-cli/commit/4e1f0e44dca106fa299b5dd0e4145c2c3a99ab4f) | fix | the request url "..." is outside of Vite serving allow list for all assets | +| [bd26a18e7](https://github.com/angular/angular-cli/commit/bd26a18e7a9512bdad15784a19f42aaca8aec303) | fix | typo in preloadInitial option description | +| [125fb779f](https://github.com/angular/angular-cli/commit/125fb779ff394f388c2d027c1dda4a33bd8caa62) | perf | reduce TypeScript JSDoc parsing in application builder | + + + + + +# 17.0.10 (2024-01-10) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------- | +| [ed1e130da](https://github.com/angular/angular-cli/commit/ed1e130dad7f9b6629f7bd31f8f0590814d0eb57) | fix | retain existing EOL when updating JSON files | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------- | +| [09c32c678](https://github.com/angular/angular-cli/commit/09c32c678221746458db50f1c2f7eb92264abb16) | fix | retain existing EOL when adding imports | +| [a5c339eaa](https://github.com/angular/angular-cli/commit/a5c339eaa73eb73e2b13558a363e058500a2cfba) | fix | retain existing EOL when updating JSON files | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [3dc4db7d7](https://github.com/angular/angular-cli/commit/3dc4db7d78649eef99a2e60b1faec8844815f8e4) | fix | retain existing EOL when updating workspace config | + + + + + +# 17.0.9 (2024-01-03) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [446dfb76a](https://github.com/angular/angular-cli/commit/446dfb76a5e2a53542fae93b4400133bf7d9552e) | fix | add prerender and ssr-dev-server schemas in angular.json schema | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [88d6ca4a5](https://github.com/angular/angular-cli/commit/88d6ca4a545c2d3e35822923f2aae03f43b2e3e3) | fix | replace template line endings with platform specific | + + + + + +# 17.0.8 (2023-12-21) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- | +| [6dba26a0b](https://github.com/angular/angular-cli/commit/6dba26a0b33ee867923c4505decd86f183e0e098) | fix | `ng e2e` and `ng lint` prompt requires to hit Enter twice to proceed on Windows | +| [0b48acc4e](https://github.com/angular/angular-cli/commit/0b48acc4eaa15460175368fdc86e3dd8484ed18b) | fix | re-add `-d` alias for `--dry-run` | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [99b026ede](https://github.com/angular/angular-cli/commit/99b026edece990e7f420718fd4967e21db838453) | fix | add missing property "buildTarget" to interface "ServeBuilderOptions" | +| [313004311](https://github.com/angular/angular-cli/commit/3130043114d3321b1304f99a4209d9da14055673) | fix | do not generate standalone component when using `ng generate module` | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [cf11cdf6c](https://github.com/angular/angular-cli/commit/cf11cdf6ce7569e2da5fa3bc76e20d19c719ce4c) | fix | add missing tailwind `@screen` directive in matcher | +| [aa6c757d7](https://github.com/angular/angular-cli/commit/aa6c757d701b7f95896c8f1643968ee030b179af) | fix | construct SSR request URL using server resolvedUrls | +| [0662048d4](https://github.com/angular/angular-cli/commit/0662048d4abbcdc36ff74d647bb7d3056dff42a8) | fix | ensure empty optimized Sass stylesheets stay empty | +| [d1923a66d](https://github.com/angular/angular-cli/commit/d1923a66d9d2ab39831ac4cd012fa0d2df66124b) | fix | ensure external dependencies are used by Web Worker bundling | + + + + + +# 16.2.11 (2023-12-21) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ----- | -------------------------------- | +| [e0e011fc4](https://github.com/angular/angular-cli/commit/e0e011fc47f2383f9be0b432066c1438ddab7103) | build | update dependency vite to v4.5.1 | + + + + + +# 17.0.7 (2023-12-13) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------ | +| [3df3e583c](https://github.com/angular/angular-cli/commit/3df3e583c8788511598bbe406012196a2882ee49) | fix | `baseHref` with trailing slash causes server not to be accessible without trailing slash | +| [ef1178188](https://github.com/angular/angular-cli/commit/ef1178188a145a1277197a33a304910e1024c365) | fix | allow vite to serve JavaScript and TypeScript assets | +| [385eb77d2](https://github.com/angular/angular-cli/commit/385eb77d2645a1407dbc7528e90a506f9bb2952f) | fix | cache loading of component resources in JIT mode | +| [4b3af73ac](https://github.com/angular/angular-cli/commit/4b3af73ac934a24dd2b022604bc01f00389d87a1) | fix | ensure browser-esbuild is used in dev server with browser builder and forceEsbuild | +| [d1b27e53e](https://github.com/angular/angular-cli/commit/d1b27e53ed9e23a0c08c13c22fc0b4c00f3998b2) | fix | ensure port 0 uses random port with Vite development server | +| [f2f7d7c70](https://github.com/angular/angular-cli/commit/f2f7d7c7073e5564ddd8a196b6fcaab7db55b110) | fix | file is missing from the TypeScript compilation with JIT | +| [7b8d6cddd](https://github.com/angular/angular-cli/commit/7b8d6cddd0daa637a5fecdea627f4154fafe23fa) | fix | handle updates of an `npm link` library from another workspace when `preserveSymlinks` is `true` | +| [c08c78cb8](https://github.com/angular/angular-cli/commit/c08c78cb8965a52887f697e12633391908a3b434) | fix | inlining of fonts results in jagged fonts for Windows users | +| [930024811](https://github.com/angular/angular-cli/commit/9300248114282a2a425b722482fdf9676b000b94) | fix | retain symlinks to output platform directories on builds | +| [3623fe911](https://github.com/angular/angular-cli/commit/3623fe9118be14eedd1a04351df5e15b3d7a289a) | fix | update ESM loader to work with Node.js 18.19.0 | + + + + + +# 17.0.6 (2023-12-06) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------- | +| [da5d39471](https://github.com/angular/angular-cli/commit/da5d39471751cd92f6c21936aefc1f7157b4973b) | fix | enable TypeScript `skipLibCheck` in new workspace | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------- | +| [048512874](https://github.com/angular/angular-cli/commit/048512874bf9cc022cc9a8ab70f35fc60d9982f5) | fix | app-shell generation incorrect content when using the application builder | +| [f9e982c44](https://github.com/angular/angular-cli/commit/f9e982c4458fc022d34039b9c082471c7ce29c07) | fix | check namespaced Sass variables when rebasing URLs | +| [a1e8ffa9d](https://github.com/angular/angular-cli/commit/a1e8ffa9df3a8eb6af2a8851385ed8927e3c0c64) | fix | correctly align error/warning messages when spinner is active | +| [46d88a034](https://github.com/angular/angular-cli/commit/46d88a034343dc93dd0c467afc08c824da427fef) | fix | handle watch updates on Mac OSX when using native FSEvents API | +| [4594407ae](https://github.com/angular/angular-cli/commit/4594407ae214ce49985a5df315cae3ac8107147d) | fix | improve file watching on Windows when using certain IDEs | +| [aa9e7c615](https://github.com/angular/angular-cli/commit/aa9e7c615529cb9dd6dccd862674cadac0372f08) | fix | normalize locale tags with Intl API when resolving in application builder | +| [a8dbf1da2](https://github.com/angular/angular-cli/commit/a8dbf1da27faf772a4df382b1301e95c32d1ba89) | fix | watch symlink when using `preserveSymlinks` option | +| [e3820cb6c](https://github.com/angular/angular-cli/commit/e3820cb6c7cf131d890882f9e94b8f23c4cbb6a3) | perf | only enable advanced optimizations with script optimizations | + + + + + +# 17.0.5 (2023-11-29) + +Rolling back [bbbe13d67](https://github.com/angular/angular-cli/commit/bbbe13d6782ba9d1b80473a98ea95bc301c48597) which appears to break file watching on Mac devices. + + + + + +# 17.0.4 (2023-11-29) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [7a2823080](https://github.com/angular/angular-cli/commit/7a2823080c61df3515d85f7aa35ee83f57e80e2d) | fix | remove CommonModule import from standalone components | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [0634a4e40](https://github.com/angular/angular-cli/commit/0634a4e40f1b2e4c0a076814f3e1b242ccf1a588) | fix | avoid native realpath in application builder | +| [22880d9cb](https://github.com/angular/angular-cli/commit/22880d9cbf70fffb6cc685b3a9ad82ca741a56fe) | fix | correct set locale when using esbuild based builders | +| [a0680672f](https://github.com/angular/angular-cli/commit/a0680672fd369dc6fba2433441d086e53bebb0a2) | fix | correctly watch files when app is in a directory that starts with a dot | +| [bbbe13d67](https://github.com/angular/angular-cli/commit/bbbe13d6782ba9d1b80473a98ea95bc301c48597) | fix | improve file watching on Windows when using certain IDEs | +| [27e7c2e1b](https://github.com/angular/angular-cli/commit/27e7c2e1b4f514843c2c505b7fe1b3cef126a101) | fix | propagate localize errors to full build result | +| [7455fdca0](https://github.com/angular/angular-cli/commit/7455fdca01bd4af00248bb1769945dc088c59063) | fix | serve assets from the provided `serve-path` | +| [657a07bd6](https://github.com/angular/angular-cli/commit/657a07bd6ba138a209c2a1540ea4d200c60e0f66) | fix | treeshake unused class that use custom decorators | +| [77474951b](https://github.com/angular/angular-cli/commit/77474951b59605a2c36a8bd890376f9e28131ee4) | fix | use workspace real path when not preserving symlinks | + + + + + +# 17.0.3 (2023-11-21) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- | +| [450dd29a1](https://github.com/angular/angular-cli/commit/450dd29a13da9930fede96732b29c9c04e1c0cf5) | fix | default to watching project root on Windows with application builder | +| [8072b8574](https://github.com/angular/angular-cli/commit/8072b8574a84a97277e8c83ebbbdde076b79a910) | fix | ensure service worker hashes index HTML file for application builder | +| [d99870740](https://github.com/angular/angular-cli/commit/d998707406c7a191a191f71d07a9491481c8ad56) | perf | only create one instance of postcss when needed | + + + + + +# 17.0.2 (2023-11-20) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------- | +| [023645185](https://github.com/angular/angular-cli/commit/02364518571a2b73be945a0036bbfa39e336330c) | fix | always normalize AOT file reference tracker paths | +| [3b99980bd](https://github.com/angular/angular-cli/commit/3b99980bd02c875a37d1603ae7468558fe7ef4c3) | fix | emit root files when `localize` is enabled when using the esbuild based builders | +| [ef3e3abb8](https://github.com/angular/angular-cli/commit/ef3e3abb8e29a9274e9d1f5fc5c18f01de6fd76f) | fix | ensure watch file paths from TypeScript are normalized | +| [d11b36fe2](https://github.com/angular/angular-cli/commit/d11b36fe207d8a38cb4a1001667c63ecd17aba0c) | fix | normalize paths in ssr sourcemaps to posix when using vite | +| [62d51383a](https://github.com/angular/angular-cli/commit/62d51383acfd8cdeedf07b34c2d78f505ff2e3a8) | fix | only include vendor sourcemaps when using the dev-server when the option is enabled | +| [d28ba8a73](https://github.com/angular/angular-cli/commit/d28ba8a7311ea3345b112a47d6f1e617fb691643) | fix | remove browser-esbuild usage warning | + + + + + +# 17.0.1 (2023-11-15) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------- | +| [5267e6055](https://github.com/angular/angular-cli/commit/5267e605567aba798ee00322f14e3a48eae68b48) | fix | handle packages with no version | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------- | +| [d9f7d439e](https://github.com/angular/angular-cli/commit/d9f7d439eba879f8fffaacd258d832c407dfd90f) | fix | add helper script to spawn SSR server from `dist` | +| [a80926cdb](https://github.com/angular/angular-cli/commit/a80926cdb6b4d99a65549fcfba2ab094a5835480) | fix | html indentation | +| [f7f62c9d6](https://github.com/angular/angular-cli/commit/f7f62c9d6988e6801981592f56137cd02bfe2316) | fix | remove `downlevelIteration` from `tsconfig.json` for new workspaces | +| [7cb57317d](https://github.com/angular/angular-cli/commit/7cb57317d2b78e9a1f947c9f11175a7d381275fc) | fix | use href property binding for links | +| [731917cd0](https://github.com/angular/angular-cli/commit/731917cd00b366bbec4f184ee9064b307eba59ce) | fix | use styleUrl | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------- | +| [15dd71aba](https://github.com/angular/angular-cli/commit/15dd71abac77ec5e1c092bebb86edffa3999937a) | fix | `deleteOutputPath` when using `esbuild-builder` | +| [fa4d8ff31](https://github.com/angular/angular-cli/commit/fa4d8ff31ef64738e45078c0e7be471591361442) | fix | add actionable error when file replacement is missing | +| [160a91160](https://github.com/angular/angular-cli/commit/160a91160ff3677d9e2d3d413ae360c4e1957c53) | fix | add support for vendor sourcemaps when using the dev-server | +| [5623c193e](https://github.com/angular/angular-cli/commit/5623c193e4cccbf6783f7e3faaf0a6c2fb086b34) | fix | cache stylesheet load errors with application builder | +| [1a5538e0c](https://github.com/angular/angular-cli/commit/1a5538e0c9cc121fa1608eb99e941bc3a5f59ad6) | fix | disable Worker wait loop for TS/NG parallel compilation in web containers | +| [883771946](https://github.com/angular/angular-cli/commit/883771946a36a42ebfe23d32b393513309b16c82) | fix | do not process ssr entry-point when running `ng serve` | +| [d3b549167](https://github.com/angular/angular-cli/commit/d3b54916705e57f017597917d9aea1f71f2ba95a) | fix | empty output directory instead of removing | +| [596f7639a](https://github.com/angular/angular-cli/commit/596f7639a6c7fe00c9088e32739578cc374a31e2) | fix | ensure compilation errors propagate to all bundle actions | +| [d900a5217](https://github.com/angular/angular-cli/commit/d900a5217a75accf434a95ad90300ec5005a23a8) | fix | maintain current watch files after build errors | +| [21549bdeb](https://github.com/angular/angular-cli/commit/21549bdeb97b23f7f37110d579513f3102dc60e8) | fix | prerender default view when no routes are defined | +| [4c251647b](https://github.com/angular/angular-cli/commit/4c251647b8fdb3b128ca3252c83aaa71ecc48e88) | fix | rewire sourcemap back to original source root | + + + + + +# 17.0.0 (2023-11-08) + +## Breaking Changes + +### @schematics/angular + +- Routing is enabled by default for new applications when using `ng generate application` and `ng new`. The `--no-routing` command line option can be used to disable this behaviour. +- `ng g interceptor` now generate a functional interceptor by default. or guard by default. To generate a class-based interceptor the `--no-functional` command flag should be used. +- `rootModuleClassName`, `rootModuleFileName` and `main` options have been removed from the public `pwa` and `app-shell` schematics. +- App-shell and Universal schematics deprecated unused `appId` option has been removed. + +### @angular-devkit/build-angular + +- Node.js v16 support has been removed + + Node.js v16 is planned to be End-of-Life on 2023-09-11. Angular will stop supporting Node.js v16 in Angular v17. + For Node.js release schedule details, please see: https://github.com/nodejs/release#release-schedule + +### @angular-devkit/schematics + +- deprecated `runExternalSchematicAsync` and `runSchematicAsync` methods have been removed in favor of `runExternalSchematic` and `runSchematic`. + +## Deprecations + +### @angular-devkit/build-angular + +- The `browserTarget` in the dev-server and extract-i18n builders have been deprecated in favor of `buildTarget`. + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [f4e7fa873](https://github.com/angular/angular-cli/commit/f4e7fa87350ea1162287114796e0e04e2af101c4) | fix | add `@angular/ssr` as part of the ng update `packageGroup` | +| [1f7156b11](https://github.com/angular/angular-cli/commit/1f7156b112606410ab9ea1cd3f178a762566b96b) | fix | add Node.js 20 as supported version | +| [4b9a87c90](https://github.com/angular/angular-cli/commit/4b9a87c90469481dc3dd0da4d1506521b4203255) | fix | ignore peer mismatch when updating @nguniversal/builders | +| [f66f9cf61](https://github.com/angular/angular-cli/commit/f66f9cf612bed49b961f1f8a8e4deef05fd5ef40) | fix | remove Node.js 16 from supported checks | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------- | +| [741cca73c](https://github.com/angular/angular-cli/commit/741cca73c129ff05e7229081d50762a054c09a8d) | feat | add `ng new --ssr` | +| [3938863b9](https://github.com/angular/angular-cli/commit/3938863b9900fcfe574b3112d73a8f34672f38bd) | feat | add migration to migrate from `@nguniversal` to `@angular/ssr` | +| [dc6b6eaf6](https://github.com/angular/angular-cli/commit/dc6b6eaf6f8af0d2b3f31cea77dc9a63ff845e3c) | feat | add migration to replace usages of `@nguniversal/builders` | +| [6979eba3c](https://github.com/angular/angular-cli/commit/6979eba3c9d46fd5fc2622d28636c48dbcbbe1c6) | feat | enable hydration when adding SSR, SSG or AppShell | +| [1a6a139aa](https://github.com/angular/angular-cli/commit/1a6a139aaf8d5a6947b399bbbd48bbfd9e52372c) | feat | enable routing by default for new applications | +| [ac0db6697](https://github.com/angular/angular-cli/commit/ac0db6697593196692e5b87e1e724be6de0ef0a0) | feat | enable standalone by default in new applications | +| [a189962a5](https://github.com/angular/angular-cli/commit/a189962a515051fd77e20bf8dd1815086a0d12ef) | feat | generate functional interceptors by default | +| [ae45c4ab8](https://github.com/angular/angular-cli/commit/ae45c4ab8103ba8ebc2686e71dbf7d0394b1ee92) | feat | update `ng new` generated application | +| [3f8aa9d8c](https://github.com/angular/angular-cli/commit/3f8aa9d8c7dc7eff06516c04ba08764bb044cb6b) | feat | update` ng new` to use the esbuild application builder based builder | +| [03a1eaf01](https://github.com/angular/angular-cli/commit/03a1eaf01c009d814cb476d2db53b2d0a4d58bcd) | fix | account for new block syntax in starter template | +| [eb0fc7434](https://github.com/angular/angular-cli/commit/eb0fc7434539d3f5a7ea3f3c4e540ac920b10c19) | fix | add missing express `REQUEST` and `RESPONSE` tokens | +| [ecdcff2db](https://github.com/angular/angular-cli/commit/ecdcff2db2b205443a585dd5dd118dbd50613883) | fix | add missing icons in ng-new template | +| [175944672](https://github.com/angular/angular-cli/commit/17594467218b788ebb27d8d16ffb0b555fcf71ee) | fix | do not add unnecessary dependency on `@angular/ssr` during migration | +| [23c4c5e42](https://github.com/angular/angular-cli/commit/23c4c5e4293ef770d555b8b2bd449ad32d1537d4) | fix | enable TypeScript `esModuleInterop` by default for ESM compliance | +| [d60a6e86a](https://github.com/angular/angular-cli/commit/d60a6e86a48f15b3ddf89943dad31ee267f67648) | fix | noop workspace config migration when already executed | +| [e516a4bdb](https://github.com/angular/angular-cli/commit/e516a4bdb7f6bb87f556e58557e57db6f7e65845) | fix | pass `ssr` option to application schematics | +| [419b5c191](https://github.com/angular/angular-cli/commit/419b5c1917c45dc115b107479d5066b9193497fa) | fix | remove `baseUrl` from `tsconfig.json` | +| [0368b23f2](https://github.com/angular/angular-cli/commit/0368b23f2e5d8ca9c6191a2db956dc6850daebfc) | fix | use @types/node v18 | +| [b15e82758](https://github.com/angular/angular-cli/commit/b15e827580d6d3159c49521eb9b5d2b6d8ca2502) | refactor | remove deprecated appId option | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------- | +| [c48982dc1](https://github.com/angular/angular-cli/commit/c48982dc1d01d11be54ffb0b1469e3b0557f3920) | feat | add `buildTarget` option to dev-server and `extract-i18n` builders | +| [1fb0350eb](https://github.com/angular/angular-cli/commit/1fb0350eb7370ef6f72acc4e20c4d0bee8bf0b29) | feat | add initial support for bundle budgets to esbuild builders | +| [8168ae2a8](https://github.com/angular/angular-cli/commit/8168ae2a892dd012707bd294ffd26d0a070c0b5d) | feat | apply global CSS updates without a live-reload when using `vite` | +| [91019bde2](https://github.com/angular/angular-cli/commit/91019bde2af5fb9dff6426ba24098271d8ac4889) | feat | enable localize support for SSR with application builder | +| [3c0719bde](https://github.com/angular/angular-cli/commit/3c0719bde244c45d71881d35899e5ee6206c09ee) | feat | initial i18n extraction support for application builder | +| [8bce80b91](https://github.com/angular/angular-cli/commit/8bce80b91b953c391ef8e45fec7f887f8d8521aa) | feat | initial support for application Web Worker discovery with esbuild | +| [49f07a84d](https://github.com/angular/angular-cli/commit/49f07a84d6f6120388d9fc48a2514d3398986e49) | feat | standardize application builder output structure | +| [c3a87a60e](https://github.com/angular/angular-cli/commit/c3a87a60e0d3cdcae9f4361c2cf21c7ea29bd7de) | feat | support basic web worker bundling with esbuild builders | +| [9e425308a](https://github.com/angular/angular-cli/commit/9e425308a0c146b685e452a106cbdf3e02bddd00) | feat | support component style budgets in esbuild builders | +| [771e036d5](https://github.com/angular/angular-cli/commit/771e036d5ce3d436736d3c8b261050d633b3ef29) | feat | support deploy URL option for `browser-esbuild` builder | +| [c5f3ec71f](https://github.com/angular/angular-cli/commit/c5f3ec71f536e7ebb1c8cd0d7523b42e58f9611a) | feat | support i18n inlining with esbuild-based builder | +| [fd62a9315](https://github.com/angular/angular-cli/commit/fd62a9315defb89b4bea996d256887a6ec7b4327) | feat | support i18n with service worker and app-shell with esbuild builders | +| [5898f72a9](https://github.com/angular/angular-cli/commit/5898f72a97c29d38b9e8b8ca23255f9fbce501e5) | feat | support namedChunks option in application builder | +| [8f9a0d70c](https://github.com/angular/angular-cli/commit/8f9a0d70cdf692b19574410cebb4d029056263fc) | feat | support standalone apps route discovery during prerendering | +| [6b08efa6f](https://github.com/angular/angular-cli/commit/6b08efa6ffd988e08e3db471ffe3214a029de116) | fix | account for arrow function IIFE | +| [2f299fc7b](https://github.com/angular/angular-cli/commit/2f299fc7b5f00056054a06574e65ae311cd3ce0c) | fix | account for styles specified as string literals and styleUrl | +| [9994b2dde](https://github.com/angular/angular-cli/commit/9994b2dde801b2f74fb70152eb73225283da32a3) | fix | add a maximum rendering timeout for SSR and SSG during development | +| [da4e19145](https://github.com/angular/angular-cli/commit/da4e19145b341dccdd5174cc7bc821e5025514b1) | fix | address a path concatenation on Windows | +| [9d4d11cc4](https://github.com/angular/angular-cli/commit/9d4d11cc43f2ae149ee8bfcf28285a1f62594ef7) | fix | allow SSR compilation to work with TS allowJs option | +| [e3c5b91e8](https://github.com/angular/angular-cli/commit/e3c5b91e8a09c8a7dd940655087b69a8949cb2cc) | fix | automatically include known packages in vite prebundling | +| [ca38ee34c](https://github.com/angular/angular-cli/commit/ca38ee34c6267e32b8ee74db815f929896f1baba) | fix | avoid binary content in architect results with browser-esbuild | +| [657f78292](https://github.com/angular/angular-cli/commit/657f78292b4c78db5a43a172087a078820812323) | fix | avoid dev server update analysis when build fails with vite | +| [2c33f09db](https://github.com/angular/angular-cli/commit/2c33f09db0561f344a26dd4f4304a9098e0ee13f) | fix | avoid dev-server proxy rewrite normalization when invalid value | +| [b182be8aa](https://github.com/angular/angular-cli/commit/b182be8aa7ff5fd3cddc0bcac5f4e45e9ed9cf2e) | fix | avoid in-memory prerendering ESM loader errors | +| [0c982b993](https://github.com/angular/angular-cli/commit/0c982b993b69f4a4b52002cc65ad7ba3b0b9d591) | fix | avoid repeat error clear in vite development server | +| [e41e2015b](https://github.com/angular/angular-cli/commit/e41e2015bfc37672fb67014ae38f31b63f0bb256) | fix | avoid spawning workers when there are no routes to prerender | +| [2d2e79921](https://github.com/angular/angular-cli/commit/2d2e79921a72c4fafad673abe501ba10400403d2) | fix | clean up internal Angular state during rendering SSR | +| [83020fc32](https://github.com/angular/angular-cli/commit/83020fc3291715802c28c5f7dcf7a261bc7f32cd) | fix | clear diagnostic cache when external templates change with esbuild builders | +| [c12f98f94](https://github.com/angular/angular-cli/commit/c12f98f948b1c10594f9d00f4ebf87630fe3cc47) | fix | conditionally enable deprecated Less stylesheet JavaScript support | +| [e10f49efa](https://github.com/angular/angular-cli/commit/e10f49efa8ac96e72bbc441423a730fd172c9f1d) | fix | convert AOT compiler exceptions into diagnostics | +| [667f43af6](https://github.com/angular/angular-cli/commit/667f43af6d91025424147f6e9ac94800f463da1d) | fix | correctly resolve polyfills when `baseUrl` URL is not set to root | +| [d46fb128a](https://github.com/angular/angular-cli/commit/d46fb128a51f172da72ab403ec97213099f43de8) | fix | disable dependency optimization for SSR | +| [1b384308c](https://github.com/angular/angular-cli/commit/1b384308c65ff67b8eac7f3b6407e19ce3db46fa) | fix | disable parallel TS/NG compilation inside WebContainers | +| [070da72c4](https://github.com/angular/angular-cli/commit/070da72c481b881538d6f5ff39955a3da7eb5126) | fix | do not perform advanced optimizations on `@angular/common/locales/global` | +| [508c7606e](https://github.com/angular/angular-cli/commit/508c7606ea2fa8e84d5243992abb59db1b75af49) | fix | do not print `Angular is running in development mode.` in the server console when running prerender in dev mode | +| [e817656f6](https://github.com/angular/angular-cli/commit/e817656f601eaaf910271d5bb2c2230ddb8ed864) | fix | do not print `Angular is running in development mode.` in the server console when running prerender in dev mode | +| [f806e3498](https://github.com/angular/angular-cli/commit/f806e3498b5a4fced7a515258fad30821f3e866c) | fix | elide setClassDebugInfo calls | +| [188a00f3e](https://github.com/angular/angular-cli/commit/188a00f3e466c6c31c7671c63ffc91ccda4590c9) | fix | elide setClassMetadataAsync calls | +| [05ce9d697](https://github.com/angular/angular-cli/commit/05ce9d697a723dcac7a5d24a14f4d11f8686851a) | fix | ensure all SSR chunks are resolved correctly with dev server | +| [d392d653c](https://github.com/angular/angular-cli/commit/d392d653cba67db28eddd003dfec6dcb9b192a95) | fix | ensure correct web worker URL resolution in vite dev server | +| [1a6aa4378](https://github.com/angular/angular-cli/commit/1a6aa437887d2fc5d08c833efc0ca792f6157350) | fix | ensure css url() prefix warnings support Sass rebasing | +| [52f595655](https://github.com/angular/angular-cli/commit/52f595655c69bb6a1398b030cf937b0d92d49864) | fix | ensure i18n locale data is included in SSR application builds | +| [3ad028bb4](https://github.com/angular/angular-cli/commit/3ad028bb442a8978a4f45511cab9bb515764b930) | fix | ensure localize polyfill and locale specifier are injected when not inlining | +| [3e5a99c2c](https://github.com/angular/angular-cli/commit/3e5a99c2c438152a0b930864dcad660a6ea1590a) | fix | ensure recalculation of component diagnostics when template changes | +| [fa234a418](https://github.com/angular/angular-cli/commit/fa234a4186c9d408bfb52b3a649d307f93d0b9b3) | fix | ensure secondary Angular compilations are unblocked on start errors | +| [c0c7dad77](https://github.com/angular/angular-cli/commit/c0c7dad77dd59a387dbcc643a52ee1ed634727ab) | fix | ensure that externalMetadata is defined | +| [ac7caa426](https://github.com/angular/angular-cli/commit/ac7caa4264c7a68467903528deca4a6f579ee15c) | fix | ensure unique internal identifiers for inline stylesheet bundling | +| [1f73bcc49](https://github.com/angular/angular-cli/commit/1f73bcc49abd9f136a18dc6329e2f50a7565eb76) | fix | ensure Web Worker code file is replaced in esbuild builders | +| [23a722b79](https://github.com/angular/angular-cli/commit/23a722b791a64bae32dc925160f2c3d1942955fc) | fix | exclude node.js built-ins from vite dependency optimization | +| [fd2c4c324](https://github.com/angular/angular-cli/commit/fd2c4c324dcfedc81af41351b52ed4c8e41f48fc) | fix | expose ssr-dev-server builder in the public api | +| [9eb58cf7a](https://github.com/angular/angular-cli/commit/9eb58cf7a51c0b7950f80b474890fb2ebd685977) | fix | fail build on non bundling error when using the esbuild based builders | +| [a3e9efe80](https://github.com/angular/angular-cli/commit/a3e9efe80f6e77c8bf80f6a2d37f4488f780503b) | fix | fully track Web Worker file changes in watch mode | +| [b9505ed09](https://github.com/angular/angular-cli/commit/b9505ed097d60eadae665d4664199e3d4989c864) | fix | generate a file containing a list of prerendered routes | +| [192a2ae6b](https://github.com/angular/angular-cli/commit/192a2ae6bd8bdeab785f1ed8e60c5e4213801dd3) | fix | handle HTTP requests to assets during prerendering | +| [19191e32b](https://github.com/angular/angular-cli/commit/19191e32bab9a2927b4feb5074e14165597fbf6d) | fix | handle HTTP requests to assets during SSG in dev-server | +| [8981d8c35](https://github.com/angular/angular-cli/commit/8981d8c355ec9154fcdcdad3a66e1b789d1079b0) | fix | improve sharing of TypeScript compilation state between various esbuild instances during rebuilds | +| [5a3ae0159](https://github.com/angular/angular-cli/commit/5a3ae0159faa81558537012a0ceba07b5ad1b88b) | fix | in vite skip SSR middleware for path with extensions | +| [f87f22d3f](https://github.com/angular/angular-cli/commit/f87f22d3f1436678ca1e07cc10874a012ae55e60) | fix | keep dependencies pre-bundling validate between builds | +| [0da87bf1c](https://github.com/angular/angular-cli/commit/0da87bf1c94c6caf711204fcdd9a3973d766bd6e) | fix | limit concurrent output file writes with application builder | +| [391ff78cb](https://github.com/angular/angular-cli/commit/391ff78cb0f29212c476ca36940b77839b84075e) | fix | log number of prerendered routes in console | +| [c46f312ad](https://github.com/angular/angular-cli/commit/c46f312adb06ae4a8293a07aa441514030052e93) | fix | media files download files in vite | +| [87425a791](https://github.com/angular/angular-cli/commit/87425a791fbdb44b3504e7e6d4b000b1df92c494) | fix | normalize paths when invalidating stylesheet bundler | +| [d4f37da50](https://github.com/angular/angular-cli/commit/d4f37da50ce2890a2b86281e5a373beab349b630) | fix | only show changed output files in watch mode with esbuild | +| [0d54f2d20](https://github.com/angular/angular-cli/commit/0d54f2d20bfd6d55615c0ab3537b5af0aeb008ee) | fix | only watch used files with application builder | +| [1f299ff2d](https://github.com/angular/angular-cli/commit/1f299ff2de3c80bf9cb3dc4b6a5ff02e81c1a94f) | fix | prebundle dependencies for SSR when using Vite | +| [58bd3971f](https://github.com/angular/angular-cli/commit/58bd3971fd2a95a5da1a87deddfe2416f3d636d6) | fix | process nested tailwind usage in application builder | +| [60ca3c82d](https://github.com/angular/angular-cli/commit/60ca3c82d28d0168b2f608a44a701ad8ad658369) | fix | provide server baseUrl result property in Vite-based dev server | +| [0c20cc4dc](https://github.com/angular/angular-cli/commit/0c20cc4dc5fe64221533d0a4cbe9d907881c85ae) | fix | re-add TestBed compileComponents in schematics to support defer block testing | +| [9453a2380](https://github.com/angular/angular-cli/commit/9453a23800f40a33b16fd887e3aa0817448134b1) | fix | remove CJS usage warnings for inactionable packages | +| [5bf7022c4](https://github.com/angular/angular-cli/commit/5bf7022c4749f1298de61ef75e36769bbb8aba12) | fix | remove support for Node.js v16 | +| [c27ad719f](https://github.com/angular/angular-cli/commit/c27ad719f2cb1b13f76f8fce033087a9124e646d) | fix | remove unactionable error overlay suggestion from Vite-based dev server | +| [263271fae](https://github.com/angular/angular-cli/commit/263271fae3f664da9d396192152d22a9b6e3ef09) | fix | resolve and load sourcemaps during prerendering to provide better stacktraces | +| [651e3195f](https://github.com/angular/angular-cli/commit/651e3195ffe06394212c8d8d275289ac05ea5ef5) | fix | resolve and load sourcemaps when using vite dev server with prerendering and ssr | +| [b78508fc8](https://github.com/angular/angular-cli/commit/b78508fc80bb9b2a3aec9830ad3ae9903d25927b) | fix | several fixes to assets and files writes in browser-esbuild builder | +| [c4c299bce](https://github.com/angular/angular-cli/commit/c4c299bce900b27556eaf2e06838a52f16990bb6) | fix | silence xhr2 not ESM module warning | +| [f7f6e97d0](https://github.com/angular/angular-cli/commit/f7f6e97d0f3540badb68813c39ce0237e4dcc9e3) | fix | skip checking CommonJS module descendants | +| [c11a0f0d3](https://github.com/angular/angular-cli/commit/c11a0f0d36f6cbffdf0464135510bda454efb08b) | fix | support custom index option paths in Vite-based dev server | +| [6c3d7d1c1](https://github.com/angular/angular-cli/commit/6c3d7d1c10907d8d57b5f84f298b324d6f972226) | fix | update `ssr` option definition | +| [4e89c3cae](https://github.com/angular/angular-cli/commit/4e89c3cae43870a10ef58de5ebdc094f5a06023e) | fix | use a dash in bundle names | +| [83b4b2567](https://github.com/angular/angular-cli/commit/83b4b25678ba6b8082d580a2d75b0f02a9addc2a) | fix | use browserslist when processing global scripts in application builder | +| [ca4d1634f](https://github.com/angular/angular-cli/commit/ca4d1634f7fa2070f53f5978387ea68cc875c986) | fix | use component style load result caching information for file watching | +| [34947fc64](https://github.com/angular/angular-cli/commit/34947fc64953f845d33ffb1c52f236869a040c9d) | fix | use incremental component style bundling only in watch mode | +| [ec160fe4e](https://github.com/angular/angular-cli/commit/ec160fe4e89cb89b93278cfac63877093dd19392) | fix | warn if using partial mode with application builder | +| [559e89159](https://github.com/angular/angular-cli/commit/559e89159150a10728272081b7bbda80fe708093) | fix | Windows Node.js 20 prerendering failure ([#26186](https://github.com/angular/angular-cli/pull/26186)) | +| [2cbec36c7](https://github.com/angular/angular-cli/commit/2cbec36c7286cdbbbd547433061421d7fe7762cc) | perf | cache polyfills virtual module result | +| [e06e95f73](https://github.com/angular/angular-cli/commit/e06e95f73a35e2cc7cb00a44ce3633b4c99c8505) | perf | conditionally add Angular compiler plugin to polyfills bundling | +| [61f409cbe](https://github.com/angular/angular-cli/commit/61f409cbe4a7bf59711ef0cfa3b7365a8df3016d) | perf | disable ahead of time prerendering in vite dev-server | +| [01ab16c5d](https://github.com/angular/angular-cli/commit/01ab16c5d5678a135a5af5640ad2ba7c33a00452) | perf | fully avoid rebuild of component stylesheets when unchanged | +| [99d9037ee](https://github.com/angular/angular-cli/commit/99d9037eee2eabd7b5ec2d8f01146578ef6b5860) | perf | only perform a server build when either prerendering, app-shell or ssr is enabled | +| [c013a95e2](https://github.com/angular/angular-cli/commit/c013a95e2f38a5c2435b22c3338bf57b03c84ebf) | perf | only rebundle browser polyfills on explicit changes | +| [e68a662bc](https://github.com/angular/angular-cli/commit/e68a662bc0e636082e43b4f3c894585174366f4d) | perf | only rebundle global scripts/styles on explicit changes | +| [28d9ab88f](https://github.com/angular/angular-cli/commit/28d9ab88fe81898ec7591608816c77455c9a61bf) | perf | only rebundle server polyfills on explicit changes | +| [6d3942723](https://github.com/angular/angular-cli/commit/6d3942723d824382e52a8f06e03dcbc3d6d8eff6) | perf | optimize server or browser only dependencies once | +| [2e8e9d802](https://github.com/angular/angular-cli/commit/2e8e9d8020aa01107a3ee6b31942d9d53d6f73cd) | perf | patch `fetch` to load assets from memory | +| [49fe74e24](https://github.com/angular/angular-cli/commit/49fe74e241d75456c65a7cd439b9eb8842e9d6d7) | perf | reduce CLI loading times by removing critters from critical path | +| [07e2120da](https://github.com/angular/angular-cli/commit/07e2120dab741fda11debc0fe777a5ef888dcaad) | perf | remove JavaScript transformer from server polyfills bundling | +| [c28475d30](https://github.com/angular/angular-cli/commit/c28475d30b08138ddddb9903acaa067cf8ab2ef6) | perf | reuse esbuild generated output file hashes | +| [59c22aa4c](https://github.com/angular/angular-cli/commit/59c22aa4cadd7bc6da20acfd3632c834824044e2) | perf | start SSR dependencies optimization before the first request | +| [223a82f5f](https://github.com/angular/angular-cli/commit/223a82f5f02c8caaf34ce49ee3ddde22a75e65c1) | perf | use incremental bundling for component styles in esbuild builders | +| [4b67d2afd](https://github.com/angular/angular-cli/commit/4b67d2afd3a2d4be188a7313b3fe4ea5c07907b6) | perf | use single JS transformer instance during dev-server prebundling | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------- | +| [f600bbc97](https://github.com/angular/angular-cli/commit/f600bbc97d30a003b9d41fa5f67590d3955e6375) | refactor | remove deprecated `runExternalSchematicAsync` and `runSchematicAsync` | + +### @angular/pwa + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------- | +| [81e4917ce](https://github.com/angular/angular-cli/commit/81e4917ceca89759770a76d63b932f380d83685c) | fix | replace Angular logos | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [dcf3fddff](https://github.com/angular/angular-cli/commit/dcf3fddff2fa4cf3433c5d726be9f514ba41e827) | feat | add performance profiler to `CommonEngine` | +| [6224b0599](https://github.com/angular/angular-cli/commit/6224b0599fd60f61c537aa602fb89079197a6e2d) | fix | correctly set config URL | +| [8d033841d](https://github.com/angular/angular-cli/commit/8d033841d1785944f60ccd425e413865c9caf581) | fix | enable `prerender` and `ssr` for all build configuration | +| [ee0991bed](https://github.com/angular/angular-cli/commit/ee0991beddc96160f9ba7e27b29def54868f3490) | fix | enable performance profiler option name | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [f43754570](https://github.com/angular/angular-cli/commit/f437545705d41c781498b8e7724293455cf3edf9) | feat | add automated preconnects for image domains | +| [4fe03266a](https://github.com/angular/angular-cli/commit/4fe03266a9232346ec49defa98d9eb3a8d88b1ff) | fix | account for arrow function IIFE | +| [828030da0](https://github.com/angular/angular-cli/commit/828030da0fa9e82fa784c4f55e3c089c7c601e98) | fix | account for styles specified as string literals and styleUrl | +| [16428fc97](https://github.com/angular/angular-cli/commit/16428fc97ae64627f790346e6b54b94a67c7202c) | fix | adjust static scan to find image domains in standlone components | +| [486becdbb](https://github.com/angular/angular-cli/commit/486becdbbaec7cacfa42bd66882efe720473b0f6) | fix | remove setClassDebugInfo calls | +| [89f21ac8c](https://github.com/angular/angular-cli/commit/89f21ac8c4309614a59cda5a8ebc3b3fbc663932) | fix | remove setClassMetadataAsync calls | +| [8899fb9e3](https://github.com/angular/angular-cli/commit/8899fb9e36556debe3b262f27c1b6e94c4963144) | fix | skip transforming empty inline styles in Webpack JIT compilations | + + + + + +# 16.2.10 (2023-11-08) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------- | +| [bab3672cd](https://github.com/angular/angular-cli/commit/bab3672cdaf4875cf83f94e34abdef29cffe2686) | fix | normalize exclude path | + + + + + +# 16.2.8 (2023-10-25) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------ | +| [44275601b](https://github.com/angular/angular-cli/commit/44275601ba0e4c7b8c24f8184a33d09350a0fbef) | fix | remove the need to specify `--migrate-only` when `--name` is used during `ng update` | + + + + + +# 16.2.7 (2023-10-19) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------ | +| [f1a0c3361](https://github.com/angular/angular-cli/commit/f1a0c3361a6caa27bdf5cc07315d8bf2b6424b11) | fix | change Twitter logo to X | + + + + + +# 16.2.6 (2023-10-11) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------- | +| [c6ea25626](https://github.com/angular/angular-cli/commit/c6ea2562683cc6e640136a02760db9363ded4352) | fix | fully downlevel async/await when using vite dev-server with caching enabled | + + + + + +# 15.2.10 (2023-10-05) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------ | +| [05213c95b](https://github.com/angular/angular-cli/commit/05213c95b032dd64fdc73ed33af695e9f19b5d09) | fix | update dependency postcss to v8.4.31 | + + + + + +# 14.2.13 (2023-10-05) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------ | +| [1ca44dcd9](https://github.com/angular/angular-cli/commit/1ca44dcd9d79916db70180da37b962c2672a76a8) | fix | update dependency postcss to v8.4.31 | + + + + + +# 16.2.5 (2023-10-04) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------------------------------- | +| [933358186](https://github.com/angular/angular-cli/commit/93335818689a67557942ab27ec8cc5b96f2a5abe) | fix | do not print `Angular is running in development mode.` in the server console when using dev-server | +| [493bd3906](https://github.com/angular/angular-cli/commit/493bd390679889359a05b2f23b74787647aee341) | fix | update dependency postcss to v8.4.31 | + + + + + +# 16.2.4 (2023-09-27) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [5dc7fb1a1](https://github.com/angular/angular-cli/commit/5dc7fb1a1849a427ceedb06404346de370c91083) | fix | update `@angular/cli` version specifier to use `^` | + + + + + +# 16.2.3 (2023-09-20) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [39643bee1](https://github.com/angular/angular-cli/commit/39643bee1522e0313be612b564f2b96ec45007ec) | fix | correctly re-point RXJS to ESM on Windows | +| [d8d116b31](https://github.com/angular/angular-cli/commit/d8d116b318377d51f258a1a23025be2d41136ee3) | fix | several windows fixes to application builder prerendering | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------- | +| [f1195d035](https://github.com/angular/angular-cli/commit/f1195d0351540bdcc7d3f3e7cf0761389eb3d569) | fix | fix recursion in webpack resolve | + + + + + +# 16.2.2 (2023-09-13) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [e3a40a49a](https://github.com/angular/angular-cli/commit/e3a40a49aa768c6b0ddce24ad47c3ba50028963c) | fix | support dev server proxy pathRewrite field in Vite-based server | + + + + + +# 16.2.1 (2023-08-30) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [221ab2483](https://github.com/angular/angular-cli/commit/221ab2483a5504b0ad864a18dc5a4dbeb8c0748e) | fix | display warning when using `resourcesOutputPath` with esbuild builder | +| [fe752ad87](https://github.com/angular/angular-cli/commit/fe752ad87b8588e2a1ee1611953b36d5c004e673) | fix | encode Sass package resolve directories in importer URLs | +| [82b0f94fd](https://github.com/angular/angular-cli/commit/82b0f94fdacc5f4665d00eeb1c93fcfc104b0cc8) | fix | handle HMR updates of global CSS when using Vite | +| [6a48a11b8](https://github.com/angular/angular-cli/commit/6a48a11b8c218796e4b778bd00d453fc0ac0c48e) | fix | update vite to be able to serve app-shell and SSG pages | +| [fdb16f7cd](https://github.com/angular/angular-cli/commit/fdb16f7cd4327980436ddb1ce190c67c86588d2d) | fix | use correct type for `extraEntryPoints` | + + + + + +# 16.2.0 (2023-08-09) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- | +| [e6b377436](https://github.com/angular/angular-cli/commit/e6b377436a471073657dc35e7c7a28db6688760a) | feat | add `ssr` option in application builder | +| [c05c83be7](https://github.com/angular/angular-cli/commit/c05c83be7c6c8bcdad4be8686a6e0701a55304cc) | feat | add initial application builder implementation | +| [095f5aba6](https://github.com/angular/angular-cli/commit/095f5aba60a4c1267a87b8b3ae38dbfbf70731f1) | feat | add initial support for server bundle generation using esbuild | +| [cb165a75d](https://github.com/angular/angular-cli/commit/cb165a75dc8c21ead537684a092ed50d3736e04a) | feat | add pre-rendering (SSG) and App-shell support generation to application builder | +| [2a3fc6846](https://github.com/angular/angular-cli/commit/2a3fc68460152a48758b9353bff48193641861c5) | feat | add preload hints based on transitive initial files | +| [099cec758](https://github.com/angular/angular-cli/commit/099cec758ad671c7fd0ca2058a271e4fe181a44d) | feat | add support for serving SSR with dev-server when using the application builder | +| [449e21b3a](https://github.com/angular/angular-cli/commit/449e21b3a6da990a5865bb5bdfb8145794f40cf9) | fix | correctly load dev server assets with vite 4.4.0+ | +| [f42f10135](https://github.com/angular/angular-cli/commit/f42f10135c1e2184a9080b726dc5e41669937b13) | fix | ensure preload hints for external stylesheets are marked as styles | +| [7defb3635](https://github.com/angular/angular-cli/commit/7defb3635c89737d151c9537bd7becd463098434) | fix | ensure that server dependencies are loaded also in ssr entrypoint | +| [05f31bd28](https://github.com/angular/angular-cli/commit/05f31bd28f002a232598e0468dc418f99e434ae0) | fix | prevent race condition in setting up sass worker pool | +| [5048f6e82](https://github.com/angular/angular-cli/commit/5048f6e82e299b0733f34cbdcb1e7b1ef9a63210) | fix | Set chunk names explicitly | +| [974748cdf](https://github.com/angular/angular-cli/commit/974748cdf894c5ad0451e3fdf1c186bdad80878b) | perf | filter postcss usage based on content in esbuild builder | +| [61a652d91](https://github.com/angular/angular-cli/commit/61a652d91274f4adce20182e630fe9963b4ceddd) | perf | inject Sass import/use directive importer information when resolving | +| [a0a2c7aef](https://github.com/angular/angular-cli/commit/a0a2c7aef675f8aae294d2119f721c4345d633b0) | perf | only load browserslist in babel preset if needed | +| [6bfd1800e](https://github.com/angular/angular-cli/commit/6bfd1800efa2bf41126696b66938bdf291ad5455) | perf | use in-memory Sass module resolution cache | + + + + + +# 16.1.8 (2023-08-04) + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------- | +| [7a420d338](https://github.com/angular/angular-cli/commit/7a420d3382b21d24c73b722e849f01b0aacfb860) | fix | build: update critters | + + + + + +# 16.1.7 (2023-08-02) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [1dab4ed87](https://github.com/angular/angular-cli/commit/1dab4ed8738b42d6b93298889caf1546b011706f) | fix | hot update filename suffix with `.mjs` | + + + + + +# 16.1.6 (2023-07-26) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [20816b57f](https://github.com/angular/angular-cli/commit/20816b57f16b0bcbd5b81f06f6f790e4485c1daa) | fix | error during critical CSS inlining for external stylesheets | + + + + + +# 16.1.5 (2023-07-20) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------- | +| [7e91d4709](https://github.com/angular/angular-cli/commit/7e91d4709966c592c271ff8d3456ce569156e2e5) | fix | add `zone.js` to `ng version` output | +| [475506822](https://github.com/angular/angular-cli/commit/475506822b148c8e2597c60653238a40140bacb0) | fix | throw an error when executed in a google3-context | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [07d3d8c6a](https://github.com/angular/angular-cli/commit/07d3d8c6ae01212de866fac769ff2da888d5adea) | fix | correctly wrap CommonJS exported enums when optimizing | + + + + + +# 16.1.4 (2023-07-06) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------- | +| [7016cee57](https://github.com/angular/angular-cli/commit/7016cee5783b2762d550db8f2a4f29e7b56f317f) | fix | normalize paths in loader cache with esbuild | + + + + + +# 16.1.3 (2023-06-29) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [b56ab0798](https://github.com/angular/angular-cli/commit/b56ab07980c5d990606ddb1e298fc1c4ecf8a2a8) | fix | use absolute watch paths for postcss dependency messages | + + + + + +# 15.2.9 (2023-06-28) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [f36e38a91](https://github.com/angular/angular-cli/commit/f36e38a913b454ec340d6bf2311391c5df1cee24) | fix | update direct semver dependencies to 7.5.3 | + + + + + +# 16.1.2 (2023-06-28) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------- | +| [3475e0281](https://github.com/angular/angular-cli/commit/3475e0281da3298f288a5f8836155c0b8c971372) | fix | update direct `semver` dependencies to 7.5.3 | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [8108b8c2d](https://github.com/angular/angular-cli/commit/8108b8c2da3ebfdb74f0f9d3554df01f484670bd) | fix | allow linker JIT support with prebundling with esbuild builder | +| [502365037](https://github.com/angular/angular-cli/commit/502365037bf7dbacd0e28d29a074a246971848ea) | fix | use all style language watch files in esbuild builder | + + + + + +# 14.2.12 (2023-06-28) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [bd396b656](https://github.com/angular/angular-cli/commit/bd396b65623fb0b8e826be13f88709e87b54336e) | fix | update direct semver dependencies to 7.5.3 | + + + + + +# 16.1.1 (2023-06-22) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------ | +| [f017fee2e](https://github.com/angular/angular-cli/commit/f017fee2e93a4207b7bfd69c838991546b398753) | fix | actually disable Vite prebundling file discovery | +| [2b4beaca2](https://github.com/angular/angular-cli/commit/2b4beaca2c32c11508078e082b3338d1edb414a0) | fix | experimental esbuild pipeline, add `es2015` to main fields for RxJS v6 compatibility | +| [e3c85e00e](https://github.com/angular/angular-cli/commit/e3c85e00e6b3390f239aaeb3db6a38fe4b4d2523) | fix | track postcss provided file dependencies in esbuild builder | +| [1419fff88](https://github.com/angular/angular-cli/commit/1419fff887173e331690fb0a664a081154842554) | fix | unpin and downgrade `browserslist` | +| [950a4b60f](https://github.com/angular/angular-cli/commit/950a4b60f046117867755ccd005f0e04bcc403a7) | fix | watch all bundler provided inputs with esbuild builder | + + + + + +# 16.1.0 (2023-06-13) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [b14b95990](https://github.com/angular/angular-cli/commit/b14b959901d5a670da0df45e082b8fd4c3392d14) | feat | add bootstrap-agnostic utilities for writing ng-add schematics | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [3ede1a2ca](https://github.com/angular/angular-cli/commit/3ede1a2cac5005f4dfbd2a62ef528a34c3793b78) | feat | allow forcing esbuild builder with dev-server | +| [2d141fe3b](https://github.com/angular/angular-cli/commit/2d141fe3bc1efb9e254b15ce91ebc885a43c928a) | feat | show estimated transfer size with esbuild builder | +| [9aa9b5264](https://github.com/angular/angular-cli/commit/9aa9b5264eee1b1dda7abd334b560d4b446c4970) | feat | support autoprefixer/tailwind CSS with Less/Sass in esbuild builder | +| [3d1c09b23](https://github.com/angular/angular-cli/commit/3d1c09b235bf1db0d031c36fdc68ab99359b34b1) | feat | support dev-server package prebundling with esbuild builder | +| [d8930facc](https://github.com/angular/angular-cli/commit/d8930facc075e39d82b3c6cb252c9a8b5fa6a476) | feat | support incremental TypeScript semantic diagnostics in esbuild builder | +| [5cacd34a2](https://github.com/angular/angular-cli/commit/5cacd34a222eea16c18caa63dbe4448b81e106f3) | fix | watch all TypeScript referenced files in esbuild builder | +| [8336ad80d](https://github.com/angular/angular-cli/commit/8336ad80da41cde69343960f7515d9ffd5e5e2e1) | perf | enable in-memory load result caching for stylesheets in esbuild builder | + + + + + +# 16.0.6 (2023-06-13) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [eebb54cbf](https://github.com/angular/angular-cli/commit/eebb54cbf4683b6113eb56dba17fab038318c918) | fix | correctly handle sass imports | +| [081b62539](https://github.com/angular/angular-cli/commit/081b62539b2562bff130343558bf4baafed7c36d) | fix | support proxy configuration array-form in esbuild builder | + + + + + +# 16.0.5 (2023-06-07) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [9817b984b](https://github.com/angular/angular-cli/commit/9817b984b15e352caedac6e347cc662117b9e0f8) | fix | ignore .git folder in browser-esbuild watcher | +| [ce95d2545](https://github.com/angular/angular-cli/commit/ce95d254510ffa93a9bd4230f6447530d511ef5f) | fix | ignore folders starting with a dot in browser-esbuild watcher | + + + + + +# 16.0.4 (2023-06-01) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [5bff97d5b](https://github.com/angular/angular-cli/commit/5bff97d5b965373cd7e4dc0b731c24d80b512faa) | fix | correctly set overridden compiler option | +| [cd0247514](https://github.com/angular/angular-cli/commit/cd0247514db295661d319bec33ad7167229d99f9) | fix | preemptively remove AOT metadata in esbuild builder | + + + + + +# 16.0.3 (2023-05-25) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [1d83bb656](https://github.com/angular/angular-cli/commit/1d83bb6565550107ab00de52b706cad8f28514b3) | fix | percent encode asset URLs in development server for esbuild | + + + + + +# 16.0.2 (2023-05-17) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [7a3c895c8](https://github.com/angular/angular-cli/commit/7a3c895c8da534ceff26754ca7ffd49b30c24069) | fix | attempt relative global script read first in esbuild builder | +| [f30be2518](https://github.com/angular/angular-cli/commit/f30be2518b118106f5d6634c92279adcefab0f70) | fix | correctly generate serviceworker hashes for binary assets | +| [117e8d001](https://github.com/angular/angular-cli/commit/117e8d00192d3b764c9c362c2554fa80706946cf) | fix | normalize Vite dev-server Windows asset paths | +| [e5c1d43de](https://github.com/angular/angular-cli/commit/e5c1d43de932daedfaac002ff363ed12243f97bb) | perf | minor sourcemap ignorelist improvements for esbuild builder | + + + + + +# 16.0.1 (2023-05-10) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------ | +| [ed82c83fe](https://github.com/angular/angular-cli/commit/ed82c83fef1a67b4168be455b119860217267564) | fix | avoid CommonJS warnings for relative imports with esbuild builders | +| [3083c4eda](https://github.com/angular/angular-cli/commit/3083c4eda87e735a4b1b9e16ff1f61abbccb1c98) | fix | avoid hash filenames for non-injected global styles/scripts | +| [b106bc9d0](https://github.com/angular/angular-cli/commit/b106bc9d07b1e2e38176c484d2fc04251e274aa5) | fix | clean incoming index URL before processing in esbuild builder | +| [2967705ed](https://github.com/angular/angular-cli/commit/2967705ed3f88c35e93866bca659222769664c62) | fix | convert dev-server glob proxy entries for esbuild builder | +| [a9d20015c](https://github.com/angular/angular-cli/commit/a9d20015c943e89b6f29a6e3e295bef6e2072a92) | fix | disable runtime errors from being displayed in overlay | +| [822b552f6](https://github.com/angular/angular-cli/commit/822b552f6f94ac1c39405f7359550e1ab5aa4c17) | fix | fix index option const value for browser-esbuild | +| [131cd23b6](https://github.com/angular/angular-cli/commit/131cd23b65c12ba671088aafcaff4d522f402ba8) | fix | prevent relative import failure with Less in esbuild builder | +| [fedcc5d92](https://github.com/angular/angular-cli/commit/fedcc5d923b7237622b1e7adef053a2ee68f872e) | fix | properly set base dev-server path with esbuild | +| [cb3161045](https://github.com/angular/angular-cli/commit/cb3161045ef39e335460672d016cf0a973de428a) | fix | show error note for CSS url() tilde usage in esbuild builder | +| [54e5000ca](https://github.com/angular/angular-cli/commit/54e5000ca88655bf9d01b87e317dc5810a7ac676) | fix | workaround for esbuild static block AOT generated code | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [5a35970af](https://github.com/angular/angular-cli/commit/5a35970afdf39461592bb0130eb9b959272949fb) | fix | do not generate an UpdateBuffer for created and overridden files | + +### @angular/pwa + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------ | +| [70d224ca7](https://github.com/angular/angular-cli/commit/70d224ca7edbfe31fb6360e55cbe06c65dc5e91a) | fix | compress PWA icons | + + + + + +# 16.0.0 (2023-05-03) + +## Breaking Changes + +### @angular/cli + +- The deprecated `defaultCollection` workspace option has been removed. Use `schematicCollections` instead. + + Before + + ```json + "defaultCollection": "@angular/material" + ``` + + After + + ```json + "schematicCollections": ["@angular/material"] + ``` + +- The deprecated `defaultProject` workspace option has been removed. The project to use will be determined from the current working directory. +- Node.js v14 support has been removed + + Node.js v14 is planned to be End-of-Life on 2023-04-30. Angular will stop supporting Node.js v14 in Angular v16. + Angular v16 will continue to officially support Node.js versions v16 and v18. + +### @schematics/angular + +- `ng g resolver` and `ng g guard` now generate a functional resolver or guard by default. It is still possible to generate a (deprecated) class-based resolver or guard by using `ng g resolver --no-functional` or `ng g guard --no-functional`. +- The CLI no longer allows to generate `CanLoad` guards. Use `CanMatch` instead. + +### + +- - TypeScript 4.8 is no longer supported. + +### @angular-devkit/build-angular + +- Deprecated `outputPath` and `outputPaths` from the server and browser builder have been removed from the builder output. Use `outputs` instead. + + Note: this change does not effect application developers. + +### @angular-devkit/core + +- Several changes to the `SchemaRegistry`. + - `compile` method now returns a `Promise`. + - Deprecated `flatten` has been removed without replacement. +- - `ContentHasMutatedException`, `InvalidUpdateRecordException`, `UnimplementedException` and `MergeConflictException` API from `@angular-devkit/core` have been removed in favor of the API from `@angular-devkit/schematics`. + - `UnsupportedPlatformException` - A custom error exception should be created instead. + +### @angular-devkit/schematics + +- The depracated `UpdateBuffer` has been removed and `UpdateBuffer2` + is renamed to `UpdateBuffer`. With this change the related and + deprecated symbols `ContentCannotBeRemovedException` and `Chunk` + have also been removed. + +### @ngtools/webpack + +- NGCC integration has been removed and as a result Angular View Engine libraries will no longer work. + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------- | +| [c2d2da41b](https://github.com/angular/angular-cli/commit/c2d2da41b15143e11f597192eef755c5e3fb4c5d) | feat | add support to add service worker to standalone application | +| [22fdd7da9](https://github.com/angular/angular-cli/commit/22fdd7da97c832048410ca89622712d097490c5d) | feat | generate functional resolvers and guards by default | +| [a832c2028](https://github.com/angular/angular-cli/commit/a832c202828a1caa425e1a0c5ff8d2ebb77c4667) | feat | Implement a standalone flag for new applications | +| [5ceedcb11](https://github.com/angular/angular-cli/commit/5ceedcb11e3ca5bdad4248c7c76ca2562fab43f2) | feat | remove deprecated CanLoad option for guards | +| [c9e84d024](https://github.com/angular/angular-cli/commit/c9e84d0243b4e9191f6cfcd72ebf8288de2b6f2d) | feat | remove generation of `BrowserModule.withServerTransition` | +| [50b9e59a5](https://github.com/angular/angular-cli/commit/50b9e59a50b737e34ee12ee48ab83d17c2b8744a) | feat | update app-shell schematic to support standalone applications | +| [dc5cc893d](https://github.com/angular/angular-cli/commit/dc5cc893d6c3d4e5e6f6c4b19bee632b66a94fc0) | feat | Update universal schematic to support standalone applications | +| [f98c9de80](https://github.com/angular/angular-cli/commit/f98c9de80952593e0294538d96bdac7136629f77) | fix | add experimental message when using standalone application schematic. | +| [a5cb46124](https://github.com/angular/angular-cli/commit/a5cb46124234ec2c47f6288914ad3ed9564f3a72) | fix | add standalone option to library library | +| [b2ed7bd10](https://github.com/angular/angular-cli/commit/b2ed7bd100bfe77dca81c590b827870fd496075f) | fix | provide migration that disables build optimizer on dev server builds | +| [ba4414b2c](https://github.com/angular/angular-cli/commit/ba4414b2cfb7a040393f314d87ab823bcad75f26) | fix | reformat app.config.ts | +| [202e9a50f](https://github.com/angular/angular-cli/commit/202e9a50f62b7927c0900469b21d323b3010762d) | fix | remove compileComponents from component test schematic | +| [0d58f73c5](https://github.com/angular/angular-cli/commit/0d58f73c50ce496dd3a0166533069f450f83a461) | fix | rename `app.server.module.ts` to `app.module.server.ts` | +| [de6d30102](https://github.com/angular/angular-cli/commit/de6d30102978eebda7edbdda43ca50f18c4c8aaf) | fix | replace `provideServerSupport` with `provideServerRendering` | +| [bff634fe0](https://github.com/angular/angular-cli/commit/bff634fe0938ecb4a316064ba3f1b9c2c1f208fe) | fix | update private Components utilities to work with standalone project structure | +| [85fe820b0](https://github.com/angular/angular-cli/commit/85fe820b081b73b229084882e98e65b5c57f9d0f) | fix | use same property order in standalone AppComponent | + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ | +| [68024234e](https://github.com/angular/angular-cli/commit/68024234edcb942d5a177d6bd7567e77d7e40245) | feat | remove deprecated `defaultCollection` from workspace configuration | +| [d58428d3d](https://github.com/angular/angular-cli/commit/d58428d3dbdb7275e2e4f6d271fcc5fdda5c489e) | feat | remove deprecated `defaultProject` from workspace configuration | +| [7cb5689e0](https://github.com/angular/angular-cli/commit/7cb5689e02c30c0ef53adef92d0e9969e1a1536b) | feat | show optional migrations during update process | +| [c29c8e18d](https://github.com/angular/angular-cli/commit/c29c8e18d84096e2f72af12643c31bde51010548) | refactor | remove Node.js v14 support | + +### + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ----- | ---------------------------------------------------------- | +| [5a171ddff](https://github.com/angular/angular-cli/commit/5a171ddff66ff366089616736baf7545fe44f570) | build | update to TypeScript 5 and drop support for TypeScript 4.8 | + +### @angular-devkit/architect + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------- | +| [48871381a](https://github.com/angular/angular-cli/commit/48871381a169888f1d29275ab25915b0d815d1c1) | fix | allow registered builder teardowns to execute | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------- | +| [ff5ebf9b1](https://github.com/angular/angular-cli/commit/ff5ebf9b1244c5a01961cd3dba6bb345392aa57c) | feat | add CSP support for inline styles | +| [ee8013f66](https://github.com/angular/angular-cli/commit/ee8013f66f7587ba85ed76fb0c662168fd850c47) | feat | display build output table with esbuild | +| [0eac98f61](https://github.com/angular/angular-cli/commit/0eac98f6176bde662d7d7e9532b5a988b8e7ece2) | feat | implement progress option for esbuild builder | +| [f04859d16](https://github.com/angular/angular-cli/commit/f04859d16117a41b6e8ad698a449aca73456b9d7) | feat | initial autoprefixer support for CSS in esbuild builder | +| [8c550302c](https://github.com/angular/angular-cli/commit/8c550302cc046e649f1245007e0e26550a61f931) | feat | initial development server for esbuild-based builder | +| [52969db6b](https://github.com/angular/angular-cli/commit/52969db6bdaf42ec7d7f28274eba518ed1a794b7) | feat | initial tailwindcss support for CSS in esbuild builder | +| [ce46ecae0](https://github.com/angular/angular-cli/commit/ce46ecae011595c86fea265e121ea313bb3cb030) | feat | support module resolution with less stylesheets in esbuild builder | +| [584b51907](https://github.com/angular/angular-cli/commit/584b51907c3b3f60db5478994fff3f800b70c3f2) | feat | support scripts option with esbuild builder | +| [e4883b0ee](https://github.com/angular/angular-cli/commit/e4883b0ee1d1ee7cd57e6cb374944021a100fd3b) | feat | support SSL options with esbuild development server | +| [290802060](https://github.com/angular/angular-cli/commit/2908020601e627b7c76c6fe8d53e19e8858cd325) | feat | support standalone app-shell generation | +| [766c14698](https://github.com/angular/angular-cli/commit/766c14698473fe333168c06e3b88c7303e868acf) | fix | add sourcemap `x_google_ignoreList` support for esbuild builder | +| [cdfa7ca88](https://github.com/angular/angular-cli/commit/cdfa7ca88c2e79564192d4b7fdafb53d97f2607d) | fix | allow multiple polyfills with esbuild-based builder | +| [e690b7cbd](https://github.com/angular/angular-cli/commit/e690b7cbde470b69b3c23fa9af1ecfca4c8e3a7e) | fix | always enable `looseEnums` build optimizer rule | +| [135ab4c36](https://github.com/angular/angular-cli/commit/135ab4c363d5d247342c4bc123a17eb66de17752) | fix | avoid double sourcemap comments with esbuild dev-server | +| [dcf60d2be](https://github.com/angular/angular-cli/commit/dcf60d2be26fdbc1efaec1c506188cb166ffbdf0) | fix | correctly filter lazy global styles in esbuild builder | +| [342a4ea30](https://github.com/angular/angular-cli/commit/342a4ea30e1ab9cbdbe5d6de339c21bdcff1a2c1) | fix | correctly show initial files in stat table with esbuild builder | +| [107851ae4](https://github.com/angular/angular-cli/commit/107851ae45d8399782cbc73d3fa09b3f779e1e02) | fix | display warning when `preserveWhitespaces` is set in the tsconfig provided to the server builder | +| [ff8a89cbf](https://github.com/angular/angular-cli/commit/ff8a89cbfd308a0312d16956d55c30e2425e2d33) | fix | ensure all build resources are served in esbuild dev server | +| [f76a8358e](https://github.com/angular/angular-cli/commit/f76a8358ea07a0d00fb0eb1c62dfaccf056531be) | fix | ensure directories are properly ignored in esbuild builder | +| [005ba4276](https://github.com/angular/angular-cli/commit/005ba427661f0e5907020aea10c432a324b528a8) | fix | ensure empty component styles compile with esbuild | +| [f74151baa](https://github.com/angular/angular-cli/commit/f74151baab740df15a5cc80255d97d0320147b2a) | fix | exclude `@angular/platform-server/init` from unsafe optimizations | +| [f72155bc7](https://github.com/angular/angular-cli/commit/f72155bc7025f4e0b23eb58a92e422bd341720f6) | fix | fully remove third-party sourcemaps when disabled in esbuild builder | +| [26dced95c](https://github.com/angular/angular-cli/commit/26dced95c5612f6386b3179fce50904f178ee569) | fix | JIT support for standalone applications | +| [4822b3ba5](https://github.com/angular/angular-cli/commit/4822b3ba55ec824913e895e76cf83e2b36ec99f9) | fix | keep esbuild server active until builder fully stops | +| [adbf2c8a1](https://github.com/angular/angular-cli/commit/adbf2c8a1ed67f505ea27921c00f957509e9a958) | fix | normalize long-form asset option output to relative path | +| [67670b612](https://github.com/angular/angular-cli/commit/67670b612e2397e26a974cd337cdce1a9c6a0f21) | fix | pass listening port in result for esbuild dev server | +| [1a8833b21](https://github.com/angular/angular-cli/commit/1a8833b211cbf2535d3deed1029591050bc995b8) | fix | provide option to run build-optimizer on server bundles | +| [b8c9667f9](https://github.com/angular/angular-cli/commit/b8c9667f9292d3829bfcac10a98acd859301c3c7) | fix | remove unintended files in esbuild output stats table | +| [04274afc1](https://github.com/angular/angular-cli/commit/04274afc15084ead2916e11055aa8f1d2f61951d) | fix | set public class fields as properties ([#24849](https://github.com/angular/angular-cli/pull/24849)) | +| [a778fe6c2](https://github.com/angular/angular-cli/commit/a778fe6c2e7b9ca0c0995e1350460e97085b39a1) | fix | show lazy files in stat table correctly with esbuild | +| [955b493b1](https://github.com/angular/angular-cli/commit/955b493b13e0a8956706c486d31d9e4338bf41c5) | fix | support CSP on critical CSS link tags. | +| [c272172c8](https://github.com/angular/angular-cli/commit/c272172c84bef35f63038f1fc5fa184b1e2d99bf) | fix | update esbuild builder complete log | +| [0b450578a](https://github.com/angular/angular-cli/commit/0b450578a74e2b46488ae2e97c7f76389baa5271) | fix | update list of known tailwind configuration files | +| [759ae92aa](https://github.com/angular/angular-cli/commit/759ae92aaa595fe3f6000f3aae0e6bb8d025db3a) | fix | update peer dependencies to support version 16 | +| [eca366a84](https://github.com/angular/angular-cli/commit/eca366a843be1fcc8d949bc335cac4cdcbdea41c) | fix | use preserveSymlinks option for tsconfigs in esbuild builder | +| [28c27567c](https://github.com/angular/angular-cli/commit/28c27567cf90712e6c8f4d483bcc0e0fc683ee9b) | perf | asynchronously delete output path in esbuild builder | +| [458400b7b](https://github.com/angular/angular-cli/commit/458400b7b1a435e2febe2c4e1a9fd1ca4eda58d0) | perf | avoid unnessary iterations | +| [a710a262a](https://github.com/angular/angular-cli/commit/a710a262aed8a6c4a6af48e0ad7f479f0a23212e) | perf | cache Sass in memory with esbuild watch mode | +| [e1398d333](https://github.com/angular/angular-cli/commit/e1398d333e86b6caad8b5cfef7048fefd77a9e22) | perf | do not inline sourcemap when using vite dev-server | +| [b2ece91b7](https://github.com/angular/angular-cli/commit/b2ece91b7488a01b6ddfcba1e68f97416c8b05f7) | perf | enhance Sass package resolution in esbuild builder | +| [aae34fc02](https://github.com/angular/angular-cli/commit/aae34fc02dc774d59ecac6483288f47074ee8c2d) | perf | fully lazy load sass in esbuild builder | +| [9ea3e8e34](https://github.com/angular/angular-cli/commit/9ea3e8e349dd1765d5935517999a1879a7a0227d) | perf | only import esbuild watcher when in watch mode | +| [f88ac6fdf](https://github.com/angular/angular-cli/commit/f88ac6fdfee6abf406720c9bc72aa9ddadb112f9) | perf | skip Angular linker in JIT mode with esbuild | +| [a99018cd7](https://github.com/angular/angular-cli/commit/a99018cd7bb66ee53026e06deae6a14455023910) | refactor | remove deprecated `outputPaths` and `outputPath` Builder output | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------- | +| [f6624b974](https://github.com/angular/angular-cli/commit/f6624b974faf13fa718d304e1a473260c16f0c1d) | feat | update SchemaRegistry `compile` to return `Promise` | +| [0ad81cdbc](https://github.com/angular/angular-cli/commit/0ad81cdbc72e80ca75d9d5cc2bc0c6163267a0bb) | refactor | remove deprecated exceptions | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ | +| [d2ef386f4](https://github.com/angular/angular-cli/commit/d2ef386f46131af904ca800cc77388c03239cd9d) | refactor | remove `UpdateBuffer` and rename `UpdateBuffer2` to `UpdateBuffer` | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ----------------------- | +| [c8ac660d8](https://github.com/angular/angular-cli/commit/c8ac660d8b13922be7ebcc92dfd5b18392602c40) | refactor | remove NGCC integration | + + + + + +# 15.2.8 (2023-05-03) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [069dcdf0c](https://github.com/angular/angular-cli/commit/069dcdf0c4e614fea83af61d4496bdd8a96920ca) | docs | improve wording in doc command version description | + + + + + +# 15.2.7 (2023-04-26) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [f4a6dac87](https://github.com/angular/angular-cli/commit/f4a6dac8782808e564678b4484f3ce87e59f6c8f) | fix | process keeps running when analytics are enabled | +| [f9b2fb1c4](https://github.com/angular/angular-cli/commit/f9b2fb1c4981ff138992a502d3aba4f6a3886df4) | perf | register CLI commands lazily | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [d9aefd6da](https://github.com/angular/angular-cli/commit/d9aefd6da5bd6ea244da3a8d5ea3dcbbadd31f99) | fix | replace vscode launch type from `pwa-chrome` to `chrome` | + + + + + +# 15.2.6 (2023-04-12) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [f0b257ef4](https://github.com/angular/angular-cli/commit/f0b257ef4ae62f92d70bfd2a4e9912d4ceff9c78) | fix | ignore hidden directories when running browserlist migration | + + + + + +# 15.2.5 (2023-04-05) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------ | +| [db173d7ed](https://github.com/angular/angular-cli/commit/db173d7edf685df67b782d81d1bacb84b8debf9a) | fix | collect tech information | + +## Special Thanks + +Alan Agius + + + + + +# 15.2.4 (2023-03-16) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------- | +| [f74bfea24](https://github.com/angular/angular-cli/commit/f74bfea241b189f261ec81a8561aea7a56774ae8) | fix | update `webpack` dependency to `5.76.1` | + +## Special Thanks + +Alan Agius + + + + + +# 14.2.11 (2023-03-16) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- | +| [ddd33bf38](https://github.com/angular/angular-cli/commit/ddd33bf38d7d76e816ebc0459559917da514477d) | fix | update webpack dependency to `5.76.1` | + +## Special Thanks + +Alan Agius and Joey Perrott + + + + + +# 13.3.11 (2023-03-16) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------- | +| [50fa9300f](https://github.com/angular/angular-cli/commit/50fa9300f264f68ad35606ae46da820c3798f665) | fix | update `webpack` dependency to `5.76.1` | + +## Special Thanks + +Alan Agius and Joey Perrott + + + + + +# 15.2.3 (2023-03-15) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [a93680585](https://github.com/angular/angular-cli/commit/a9368058517509b277236d6e7db4abc6248817fa) | fix | correct wrap ES2022 classes with static properties | + +## Special Thanks + +Alan Agius and Paul Gschwendtner + + + + + +# 15.2.2 (2023-03-08) + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------- | +| [dfd03aa7c](https://github.com/angular/angular-cli/commit/dfd03aa7c262f4425fa680e205a46792bd7b8451) | fix | correctly transform numbers from prompts | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------- | +| [eb22f634f](https://github.com/angular/angular-cli/commit/eb22f634f2ec7a5b0bc2f5300682ed8e718b1424) | fix | build optimizer support for non spec-compliant ES2022 class static properties | + +## Special Thanks + +Alan Agius + + + + + +# 15.2.1 (2023-03-01) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------- | +| [9a5609a44](https://github.com/angular/angular-cli/commit/9a5609a440fc49b3f7ddf88efb73618b7eede1ea) | fix | improve parsing of error messages | + +## Special Thanks + +Alan Agius and Paul Gschwendtner + + + + + +# 15.2.0 (2023-02-22) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------- | +| [0f58a17c4](https://github.com/angular/angular-cli/commit/0f58a17c4ce92495d96721bc3f2b632a890bbab4) | feat | log number of files update during `ng update` | + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------------- | +| [ecf43090d](https://github.com/angular/angular-cli/commit/ecf43090d110f996f45a259c279f1b83dcab3fd8) | feat | auto detect package manager ([#24305](https://github.com/angular/angular-cli/pull/24305)) | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [01b3bcf89](https://github.com/angular/angular-cli/commit/01b3bcf898108f9b879da4a791fa2a21c6d9f7c5) | feat | add Less stylesheet support to experimental esbuild-based builder | +| [09af70743](https://github.com/angular/angular-cli/commit/09af70743800aefdefe06e0ca32bcdde18f9eb77) | feat | implement node module license extraction for esbuild builder | +| [bbc1a4f0d](https://github.com/angular/angular-cli/commit/bbc1a4f0dc93437fe97a53a35f68d978cc50bb9e) | feat | support CommonJS dependency checking in esbuild | +| [8cf0d17fb](https://github.com/angular/angular-cli/commit/8cf0d17fb1b39ea7bbd1c751995a56de3df45114) | feat | support JIT compilation with esbuild | +| [3f6769ef9](https://github.com/angular/angular-cli/commit/3f6769ef953b1f880508a9152e669064cbb4dcc9) | fix | allow empty scripts to be optimized | +| [421417a36](https://github.com/angular/angular-cli/commit/421417a36b13a44d39e0818171482871ea8b895f) | fix | avoid CommonJS warning for zone.js in esbuild | + +## Special Thanks + +Alan Agius, Charles Lyding, Doug Parker, Jason Bedard, Joey Perrott, Marvin and Paul Gschwendtner + + + + + +# 15.1.6 (2023-02-15) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | +| [3d1f48fa2](https://github.com/angular/angular-cli/commit/3d1f48fa2991ded75da3a1b3a431480710a8ce15) | fix | add set `SessionEngaged` in GA | +| [df07ab113](https://github.com/angular/angular-cli/commit/df07ab11351d6f2d82922ae251ccd17b23d9d0a9) | fix | convert `before` option in `.npmrc` to Date | +| [c787cc780](https://github.com/angular/angular-cli/commit/c787cc7803598eb67260cbd2112d411384d518cc) | fix | replace `os.version` with `os.release`. | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [34a4a1bbf](https://github.com/angular/angular-cli/commit/34a4a1bbf608eb54b0a33b3aa3a6be3e2a576770) | fix | correctly copy `safety-worker.js` contents | +| [88a33155d](https://github.com/angular/angular-cli/commit/88a33155d4bc00077d32bef42588427fb2ed49f4) | fix | update the ECMA output warning message to be more actionable | +| [384ad29c9](https://github.com/angular/angular-cli/commit/384ad29c9a66d78e545ed7e48bf962e4df9d0549) | fix | use babel default export helper in build optimizer | +| [59aa1cdbd](https://github.com/angular/angular-cli/commit/59aa1cdbdf3e2712f988790f68bacc174d070b0c) | perf | reduce rebuilt times when using the `scripts` option | + +## Special Thanks + +Alan Agius and Charles Lyding + + + + + +# 15.1.5 (2023-02-08) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [b8bbe9688](https://github.com/angular/angular-cli/commit/b8bbe9688e0e684245636e7d58d50c51719039c8) | fix | error if Angular compiler is used in a schematic | +| [fabbb8a93](https://github.com/angular/angular-cli/commit/fabbb8a936f3b3b1cee8ea5cbdb7bb7832cb02a7) | fix | only set `DebugView` when `NG_DEBUG` is passed | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [499173b5d](https://github.com/angular/angular-cli/commit/499173b5d197f14377203b92b49ff3cbbf55b260) | fix | remove bootstrapping wrapping in universal schematic | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [e87134fe9](https://github.com/angular/angular-cli/commit/e87134fe94831df76698fe0e90fe556da0011511) | fix | build optimizer support for spec-compliant downlevel class properties | +| [d80adde2f](https://github.com/angular/angular-cli/commit/d80adde2fec53e6513983a89dd194a35c426b8aa) | fix | do not fail compilation when spec pattern does not match | +| [11be502e7](https://github.com/angular/angular-cli/commit/11be502e7cc2544371d55c8b3d32b7bcbbf8066e) | fix | fix support of Safari TP versions | +| [14e317d85](https://github.com/angular/angular-cli/commit/14e317d85429c83e6285c5cec4a1c4483d8a1c8f) | fix | load polyfills and runtime as scripts instead of modules | + +## Special Thanks + +Alan Agius, Charles Lyding, Kristiyan Kostadinov and Ricardo + + + + + +# 15.1.4 (2023-02-01) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | +| [6c8fdfc69](https://github.com/angular/angular-cli/commit/6c8fdfc6985c5b5017a0b6ab6fa38daf4cb9a775) | fix | load JavaScript bundles as modules in karma | +| [317452e3b](https://github.com/angular/angular-cli/commit/317452e3b7e25080132b7f7a069696d1c5054f69) | fix | print server builder errors and warnings | + +## Special Thanks + +Alan Agius + + + + + +# 15.1.3 (2023-01-25) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [de15ec576](https://github.com/angular/angular-cli/commit/de15ec5763afe231439c3f1ace35cbacefad2ca7) | fix | handle extended schematics when retrieving aliases | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [2c04f4a8f](https://github.com/angular/angular-cli/commit/2c04f4a8f493781fda65f31e81ad86cdd3e510c0) | fix | update browserslist config to include last 2 Chrome version | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [f31bf300b](https://github.com/angular/angular-cli/commit/f31bf300b9f226d9574060b0e4401c4da88c0ee3) | fix | avoid undefined module path for Sass imports in esbuild | +| [c152a4a13](https://github.com/angular/angular-cli/commit/c152a4a13f482948c6aedbbc99d1423f2cf43aea) | fix | update browserslist config to include last 2 Chrome versions | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [9de99202e](https://github.com/angular/angular-cli/commit/9de99202e9427973c7983940fcdea9e4580a79bd) | fix | handle number like strings in workspace writer | + +## Special Thanks + +Alan Agius, Charles Lyding and Doug Parker + + + + + +# 15.1.2 (2023-01-18) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------- | +| [387472a95](https://github.com/angular/angular-cli/commit/387472a956b71eaca89e210e64f4d75969abc9d3) | fix | register schematic aliases when providing collection name in `ng generate` | +| [5d9fd788a](https://github.com/angular/angular-cli/commit/5d9fd788a997066dea1b2d69dced865a7c60f5c1) | fix | remove `--to` option from being required when using `--from` in `ng update` | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------------------------- | +| [0f5fb7e59](https://github.com/angular/angular-cli/commit/0f5fb7e5944e3a521758c67f403d71928f93f7ac) | fix | replace existing `BrowserModule.withServerTransition` calls when running universal schematic | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [bf4639a6e](https://github.com/angular/angular-cli/commit/bf4639a6e97670972c3d5b137230e2f08467010e) | fix | prevent hanging initial build during exception with esbuild | + +## Special Thanks + +Alan Agius, Charles Lyding and Doug Parker + + + + + +# 15.1.1 (2023-01-12) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------- | +| [b94bf60ca](https://github.com/angular/angular-cli/commit/b94bf60ca828a22d548d65b819ea745eafb96deb) | fix | update `esbuild` to `0.16.17` | + +## Special Thanks + +Alan Agius + + + + + +# 15.1.0 (2023-01-11) + +## Deprecations + +### @angular-devkit/schematics + +- The Observable based `SchematicTestRunner.runSchematicAsync` and `SchematicTestRunner.runExternalSchematicAsync` method have been deprecated in favor of the Promise based `SchematicTestRunner.runSchematic` and `SchematicTestRunner.runExternalSchematic`. + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------- | +| [5b18ce154](https://github.com/angular/angular-cli/commit/5b18ce1545d047d49851a64e81a1f8ef59624ef7) | feat | add `guardType` as an alias of `implements` in guard schematic | +| [dd2b65943](https://github.com/angular/angular-cli/commit/dd2b65943d706833f449f76cf8c7278d0a5399ad) | feat | add configuration files generation schematic | +| [8d000d156](https://github.com/angular/angular-cli/commit/8d000d1563684f9a9b6869e549e265f0997187c4) | feat | add environments generation schematic | +| [6c39a162b](https://github.com/angular/angular-cli/commit/6c39a162bec67083bf6c11b54e84612f1d68c384) | feat | Add schematics for generating functional router guards and resolvers | +| [62121f89a](https://github.com/angular/angular-cli/commit/62121f89abce54e0a1c2b816cdd32b57f2b5a5d1) | feat | add sideEffects:false to library package.json | +| [9299dea64](https://github.com/angular/angular-cli/commit/9299dea6492527bcaea24c9c7f3116ee2779405b) | feat | generate functional interceptors | +| [49b313f27](https://github.com/angular/angular-cli/commit/49b313f27adef6300063c9d6817d1454a8657fe2) | fix | add missing import for functional interceptor spec | +| [2f92fe7e5](https://github.com/angular/angular-cli/commit/2f92fe7e589705b282102271897454ea852c4814) | fix | add missing semicolon in functional guard/resolver/interceptor | +| [9b6d190f4](https://github.com/angular/angular-cli/commit/9b6d190f4a082c166d253b0f00162e0286238e45) | fix | remove EnvironmentInjector import in functional guard spec | +| [b11d3f644](https://github.com/angular/angular-cli/commit/b11d3f6442d38f609471ab19c08a1c9a871e0ae3) | fix | use proper variable in functional guard spec | +| [451975f76](https://github.com/angular/angular-cli/commit/451975f7650041a83994e1308f85fe7e33a31e32) | fix | use proper variable in resolver functional spec | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [c29df6954](https://github.com/angular/angular-cli/commit/c29df695467c41feccd3846a55c91c6784af87b2) | feat | add `assets` option to server builder | +| [839d0cb57](https://github.com/angular/angular-cli/commit/839d0cb57ad42896578c235354ffb918ea8bb146) | feat | implement stats-json option for esbuild builder | +| [216991b9d](https://github.com/angular/angular-cli/commit/216991b9d9ca1d8f09992880a5fa92e7c98813fa) | feat | support inline component Sass styles with esbuild builder | +| [7c87ce47c](https://github.com/angular/angular-cli/commit/7c87ce47c66a6426b6b7fbb2edd38d8da729221f) | fix | ensure Sass load paths are resolved from workspace root | +| [7a063238b](https://github.com/angular/angular-cli/commit/7a063238b83eea8b5b3237fed12db5528d1f6912) | fix | explicitly send options to JS transformer workers | +| [22cba7937](https://github.com/angular/angular-cli/commit/22cba79370ed60a27f932acda363ffd87f5d9983) | fix | provide an option to `exclude` specs in Karma builder | +| [20376649c](https://github.com/angular/angular-cli/commit/20376649c5e3003b0aa99b9328e2b61699ccba78) | fix | transform async generator class methods for Zone.js support | +| [0520608f6](https://github.com/angular/angular-cli/commit/0520608f68f1768a13a46fbdb9ecb65310492460) | fix | use relative css resource paths in esbuild JSON stats | +| [0c01532cb](https://github.com/angular/angular-cli/commit/0c01532cb5a3072b96cd65845a38b88ed4543de6) | perf | use worker pool for JavaScript transforms in esbuild builder | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [207358afb](https://github.com/angular/angular-cli/commit/207358afb89e6515cb8d73f5a3a63d9101e80d97) | feat | add `runSchematic` and `runExternalSchematic` methods | + +## Special Thanks + +Alan Agius, Andrew Scott, Charles Lyding, Cédric Exbrayat, Doug Parker, Felix Hamann, Jason Bedard, Joey Perrott and Kristiyan Kostadinov + + + + + +# 15.0.5 (2023-01-06) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [c2030dec7](https://github.com/angular/angular-cli/commit/c2030dec7d9fecf42cca2de37cc3f7adaaa45e7f) | fix | format esbuild error messages to include more information | + +## Special Thanks + +Alan Agius, Kristiyan Kostadinov, Paul Gschwendtner and aanchal + + + + + +# 15.0.4 (2022-12-14) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [ccc8e0350](https://github.com/angular/angular-cli/commit/ccc8e0350810d123269f55de29acd7964e663f7e) | fix | display actionable error when a style does not exist in Karma builder | +| [507f756c3](https://github.com/angular/angular-cli/commit/507f756c34171db842365398150460e1e29f531a) | fix | downlevel class private methods when targeting Safari <=v15 | +| [a0da91dba](https://github.com/angular/angular-cli/commit/a0da91dba3d9b4c4a86102668f52ab933406e5da) | fix | include sources in generated | +| [9fd356234](https://github.com/angular/angular-cli/commit/9fd356234210734ec5f44ae18f055308b7acc963) | fix | only set ngDevMode when script optimizations are enabled | +| [8e85f4728](https://github.com/angular/angular-cli/commit/8e85f47284472f9df49f2ca6c59057ad28240e9c) | fix | update `css-loader` to `6.7.3` | +| [b2d4415ca](https://github.com/angular/angular-cli/commit/b2d4415caa486bebe55e6147a153f120cf08b070) | fix | update locale setting snippet to use `globalThis`. | + +## Special Thanks + +Alan Agius and Charles Lyding + + + + + +# 15.0.3 (2022-12-07) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [3d9971edb](https://github.com/angular/angular-cli/commit/3d9971edb05e9b8de24bafc1b4381cbf4bad8dbf) | fix | default preserve symlinks to Node.js value for esbuild | +| [24f4b51d2](https://github.com/angular/angular-cli/commit/24f4b51d22a0debc8ff853cf9040a15273654f7a) | fix | downlevel class fields with Safari <= v15 for esbuild | +| [45afc42db](https://github.com/angular/angular-cli/commit/45afc42db86e58357d1618d9984dcf03bffea957) | fix | downlevel class properties when targeting Safari <=v15 | +| [e6461badf](https://github.com/angular/angular-cli/commit/e6461badf7959ff8b8d9a3824a4a081f44e0b237) | fix | prevent optimization adding unsupported ECMASCript features | + +## Special Thanks + +Charles Lyding, Dominic Elm and Paul Gschwendtner + + + + + +# 15.0.2 (2022-11-30) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [2891d5bc9](https://github.com/angular/angular-cli/commit/2891d5bc9eecf7fa8e3b80906d9c56e6a49f3d15) | fix | correctly set Sass quietDeps and verbose options | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [d9cc4b028](https://github.com/angular/angular-cli/commit/d9cc4b0289eaf382782a994a15497e9526c5a4a2) | fix | elide unused type references | + +## Special Thanks + +Alan Agius and Juuso Valkeejärvi + + + + + +# 15.0.1 (2022-11-23) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [eda96def4](https://github.com/angular/angular-cli/commit/eda96def48e11533cd0a3353c96b7eac9a881e1e) | fix | use global version of the CLI when running `ng new` | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [48426852b](https://github.com/angular/angular-cli/commit/48426852b0c1d5541a3e7369dc2b343e33856968) | fix | show warning when a TS Config is not found during migrations | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------- | +| [2af32fd3a](https://github.com/angular/angular-cli/commit/2af32fd3a981b1c29e1cf77b442982e1e07aae38) | fix | hide loader paths in webpack warnings | +| [19f5cc746](https://github.com/angular/angular-cli/commit/19f5cc746ec724f15d1b89126c7c1b8a343818fe) | fix | improve package deep import Sass index resolution in esbuild plugin | +| [2220a907d](https://github.com/angular/angular-cli/commit/2220a907daf9ccd9e22dfc8e5ddc259b9d495997) | fix | use url function lexer to rebase Sass URLs | + +## Special Thanks + +Alan Agius, Charles Lyding, Doug Parker, Joey Perrott and Piotr Wysocki + + + + + +# 15.0.0 (2022-11-16) + +## Breaking Changes + +### @angular/cli + +- The Angular CLI no longer supports `16.10.x`, `16.11.x` and `16.12.x`. Current minimum versions of Node.js are `14.20.0`, `16.13.0` and `18.10.0`. +- Node.js versions older than 14.20 are no longer supported. +- The 'path' option in schematics schema no longer has a special meaning. Use 'workingDirectory' smart default provider should be used instead. + +### @schematics/angular + +- Removed unused `appDir` option from Universal and App-Shell schematic. This option can safely be removed if present since it no longer has effect. + +### + +- `analyticsSharing` option in the global angular configuration has been + removed without replacement. This option was used to configure the Angular CLI to access to your own users' CLI usage data. + + If this option is used, it can be removed using `ng config --global cli.analyticsSharing undefined`. + +- analytics APIs have been removed without replacement from `@angular-devkit/core` and `@angular-devkit/architect`. + +### @angular-devkit/build-angular + +- TypeScript versions older than 4.8.2 are no longer supported. +- The server builder `bundleDependencies` option has been removed. This option was used pre Ivy. Currently, using this option is unlikely to produce working server bundles. + + The `externalDependencies` option can be used instead to exclude specific node_module packages from the final bundle. + +- - Deprecated support for tilde import has been removed. Please update the imports by removing the `~`. + + Before + + ```scss + @import 'http://github.com/~font-awesome/scss/font-awesome'; + ``` + + After + + ```scss + @import 'http://github.com/font-awesome/scss/font-awesome'; + ``` + + - By default the CLI will use Sass modern API, While not recommended, users can still opt to use legacy API by setting `NG_BUILD_LEGACY_SASS=1`. + +- Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration. +- `require.context` are no longer parsed. Webpack specific features are not supported nor guaranteed to work in the future. +- Producing ES5 output is no longer possible. This was needed for Internet Explorer which is no longer supported. All browsers that Angular supports work with ES2015+ +- server builder `bundleDependencies` option now only accept a boolean value. +- Deprecated support for Stylus has been removed. The Stylus package has never reached a stable version and its usage in the Angular CLI is minimal. It's recommended to migrate to another CSS preprocessor that the Angular CLI supports. + +### @angular-devkit/core + +- Workspace projects with missing `root` is now an error. + +### @ngtools/webpack + +- TypeScript versions older than 4.8.2 are no longer supported. + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ | +| [766d4a089](https://github.com/angular/angular-cli/commit/766d4a0895e7895211e93bc73ff131c6e47613a7) | feat | add migration to remove require calls from karma builder main file | +| [d8bff4f1e](https://github.com/angular/angular-cli/commit/d8bff4f1e68a76da1983f9d0774f415e73dfd8c3) | feat | Added --project-root option to the library schematics | +| [597bfea1b](https://github.com/angular/angular-cli/commit/597bfea1b29cc7b25d1f466eb313cbeeb6dffc98) | feat | drop `polyfills.ts` file from new templates | +| [1c21e470c](https://github.com/angular/angular-cli/commit/1c21e470c76d69d08e5096b46b952dbce330f7ef) | feat | enable error on unknown properties and elements in tests | +| [f2a0682dc](https://github.com/angular/angular-cli/commit/f2a0682dc82afa23a3d3481df59e4aaca5e90c78) | feat | generate new projects using TypeScript 4.8.2 | +| [b06421d15](https://github.com/angular/angular-cli/commit/b06421d15e4b5e6daffcb73ee1c2c8703b72cb47) | feat | mark `projectRoot` as non hidden option in application schematic | +| [b6897dbb0](https://github.com/angular/angular-cli/commit/b6897dbb0a1ef287644e117251c1c76cc8afcae0) | feat | remove `karma.conf.js` from newly generated projects | +| [301b5669a](https://github.com/angular/angular-cli/commit/301b5669a724261d53444d5172334966903078c0) | feat | remove `ngOnInit` from component template | +| [9beb878e2](https://github.com/angular/angular-cli/commit/9beb878e2eecd32e499c8af557f22f46548248fc) | feat | remove Browserslist configuration files from projects | +| [283b564d1](https://github.com/angular/angular-cli/commit/283b564d1de985f0af8c2fcb6192801a90baacda) | feat | remove environment files in new applications | +| [56a1e8f9f](https://github.com/angular/angular-cli/commit/56a1e8f9f52658488afb9d36007e96c96d08a03b) | feat | remove test.ts file from new projects | +| [4e69e8050](https://github.com/angular/angular-cli/commit/4e69e80501dd2a9394b7df4518e0d6b0f2ebb7d9) | fix | add `@angular/localize` as type when localize package is installed | +| [57d93fb7d](https://github.com/angular/angular-cli/commit/57d93fb7d979e68c2a4e6f6046ff633f69098afe) | fix | mark project as required option | +| [84e3f7727](https://github.com/angular/angular-cli/commit/84e3f7727dc1de31484704c7c06d51ff5392a34a) | fix | remove empty lines | +| [316a50d75](https://github.com/angular/angular-cli/commit/316a50d75e45962ea3efe4108aa48d9479245dd5) | fix | remove TypeScript target from universal schematic | +| [69b221498](https://github.com/angular/angular-cli/commit/69b2214987c8fad6efd091782cf28b20be62d244) | refactor | remove deprecated appDir option | + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- | +| [4827d1b23](https://github.com/angular/angular-cli/commit/4827d1b23e564e4e4a8684c5e8ff035d8fa855a2) | feat | add support for Node.js version 18 | +| [4b623461a](https://github.com/angular/angular-cli/commit/4b623461a4a938ba320b5e019f9c715d634a46c4) | feat | drop support for Node.js versions older than 14.20 | +| [3dea1fa71](https://github.com/angular/angular-cli/commit/3dea1fa7173e846aff5b0d15b919d9786bbf7198) | fix | add unique user id as user parameter in GA | +| [af07aa340](https://github.com/angular/angular-cli/commit/af07aa340a1c3c9f3d42446981be59a73effa498) | fix | add workspace information as part of analytics collection | +| [83524f625](https://github.com/angular/angular-cli/commit/83524f62533f9a6bda0c1dbc76c6b16e730a7397) | fix | allow `ng add` to find prerelease versions when CLI is prerelease | +| [22955f245](https://github.com/angular/angular-cli/commit/22955f24592df8044dbdeeb8e635beb1cc770c75) | fix | do not collect analytics when running in non TTY mode | +| [35e5f4278](https://github.com/angular/angular-cli/commit/35e5f4278145b7ef55a75f1692c8e92d6bcd59db) | fix | exclude `@angular/localize@<10.0.0` from ng add pa… ([#24152](https://github.com/angular/angular-cli/pull/24152)) | +| [1a584364e](https://github.com/angular/angular-cli/commit/1a584364e70cafd84770ef45f3da9ad58a46083f) | fix | exclude `@angular/material@7.x` from ng add package discovery | +| [ff0382718](https://github.com/angular/angular-cli/commit/ff0382718af60923fe71f8b224d36a50449484e6) | fix | respect registry in RC when running update through yarn | +| [774d349b7](https://github.com/angular/angular-cli/commit/774d349b73a436a99f2ea932b7509dab7c1d5e45) | refactor | remove deprecated path handler | + +### + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------- | +| [639a3071c](https://github.com/angular/angular-cli/commit/639a3071c3630c1ccdf7e3c015e81e9423ab2678) | refactor | migrate analytics collector to use GA4 | +| [c969152de](https://github.com/angular/angular-cli/commit/c969152de630a9afdef44ba2342e728b9353c8e7) | refactor | remove analytics API from core and architect | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------- | +| [4ead45cab](https://github.com/angular/angular-cli/commit/4ead45caba08cb0b67dc7df2f6a9b304c75fff7d) | feat | add `ng-server-context` when using app-shell builder | +| [1c527a9da](https://github.com/angular/angular-cli/commit/1c527a9da5b55a8421ebca787fd322e879f6d29d) | feat | add esbuild-based builder initial support for fileReplacements | +| [67324b3e5](https://github.com/angular/angular-cli/commit/67324b3e5861510b1df9641bb4b10bb67e3a2325) | feat | add initial incremental code rebuilding to esbuild builder | +| [3d94ca21b](https://github.com/angular/angular-cli/commit/3d94ca21bbb7496a2ff588166fd93c5f2339b823) | feat | add initial watch support to esbuild-based builder | +| [c592ec584](https://github.com/angular/angular-cli/commit/c592ec584f1c0b126a2045e5ea1b01cb1569ce4d) | feat | amend `polyfills` option in all builders to support an array of module specifiers | +| [a95d130ef](https://github.com/angular/angular-cli/commit/a95d130ef4249457ed2433d52eb43c94a1169782) | feat | auto include `@angular/localize/init` when found in `types` | +| [979bce45e](https://github.com/angular/angular-cli/commit/979bce45e63eda9ac5402869ef3dc4c63aaca3f1) | feat | auto include `@angular/platform-server/init` during server builds | +| [fd4175357](https://github.com/angular/angular-cli/commit/fd41753579affa78328bfc4b6108db15ff5053f9) | feat | drop support for TypeScript 4.6 and 4.7 | +| [15d3fc6dc](https://github.com/angular/angular-cli/commit/15d3fc6dc3f74462818b3745f6fb4995212a4d22) | feat | export `@angular/platform-server` symbols in server bundle | +| [05a98c029](https://github.com/angular/angular-cli/commit/05a98c02924f656be3257d5f459ae88c1ae29fba) | feat | karma builder `main` option is now optional | +| [2b6029245](https://github.com/angular/angular-cli/commit/2b602924538bf987e92f806c25c2a3d008a3f0a9) | feat | providing a karma config is now optional | +| [9c13fce16](https://github.com/angular/angular-cli/commit/9c13fce162eff8d01d1fa6a7f0e0029da2887c86) | feat | remove `bundleDependencies` from server builder | +| [308e3a017](https://github.com/angular/angular-cli/commit/308e3a017f876bfc727e68803bfbce11e9d3396e) | feat | switch to use Sass modern API | +| [1e5d4a750](https://github.com/angular/angular-cli/commit/1e5d4a75084dfd2aeebb6a0c0b3039417e14bc84) | feat | use Browserslist to determine ECMA output | +| [3ff391738](https://github.com/angular/angular-cli/commit/3ff39173808f2beed97ee5deb91be541205f9a03) | fix | account for package.json exports fields with CSS import statements | +| [001445982](https://github.com/angular/angular-cli/commit/0014459820dc1c127e93993414c154947a7f8da6) | fix | account for package.json exports with Sass in esbuild builder | +| [6280741ce](https://github.com/angular/angular-cli/commit/6280741ce4a89882595c834f48a45cca6f9534e0) | fix | add `@angular/platform-server` as an optional peer dependency | +| [f9a2c3a12](https://github.com/angular/angular-cli/commit/f9a2c3a1216cf9510e122df44a64ddd11d47226b) | fix | allow both script and module sourceTypes to be localized | +| [4cb27b803](https://github.com/angular/angular-cli/commit/4cb27b8031d0f36e687c5116538ebe473acaa149) | fix | avoid attempted resolve of external CSS URLs with esbuild builder | +| [192e0e6d7](https://github.com/angular/angular-cli/commit/192e0e6d77d4f0f20af3f88b653c5196a2c1e052) | fix | correct escaping of target warning text in esbuild builder | +| [4fcb0a82b](https://github.com/angular/angular-cli/commit/4fcb0a82b5fa8a092d8c374cdea448edd80270d4) | fix | correctly resolve Sass partial files in node packages | +| [fb5a66ae6](https://github.com/angular/angular-cli/commit/fb5a66ae66b595602d2a8aea8e938efe5df6d13c) | fix | fix crash when Sass error occurs | +| [b6df9c136](https://github.com/angular/angular-cli/commit/b6df9c1367ae5795a3895628ec9822d432b315bb) | fix | handle conditional exports in `scripts` and `styles` option | +| [0ee7625d6](https://github.com/angular/angular-cli/commit/0ee7625d6b4bd84be6fca0df82f3e74e4b94728c) | fix | ignore cache path when watching with esbuild builder | +| [e34bfe5eb](https://github.com/angular/angular-cli/commit/e34bfe5eb1a559cbf53449ce213503e32fa27ae4) | fix | ignore specs in node_modules when finding specs | +| [f143171fd](https://github.com/angular/angular-cli/commit/f143171fd030fa1cc8df84ed5f0b96f5ad0f9e10) | fix | only add `@angular/platform-server/init` when package is installed. | +| [3a1970b76](https://github.com/angular/angular-cli/commit/3a1970b76e4da7424e2661664a1e9e669bd279b4) | fix | only import karma when running karma builder | +| [8b84c18ed](https://github.com/angular/angular-cli/commit/8b84c18edd01e91c7ebf4327dde8ce60f7f700ca) | fix | provide workaround for V8 object spread performance defect | +| [7dd122ad5](https://github.com/angular/angular-cli/commit/7dd122ad5f34a488f3784326b579b8a93511af7e) | fix | rebase Sass url() values when using esbuild-based builder | +| [2105964af](https://github.com/angular/angular-cli/commit/2105964afc0285cc40c16d32c47d1eb60be5e279) | fix | resolve transitive dependencies in Sass when using Yarn PNP | +| [54e1c01d8](https://github.com/angular/angular-cli/commit/54e1c01d8b608ff240f7559ca176cd50e991952c) | fix | show file replacement in TS missing file error in esbuild builder | +| [6c3f281d9](https://github.com/angular/angular-cli/commit/6c3f281d927c9ae2d4ec76ff9f920752e2cb73d1) | fix | show warning when using TypeScript target older then ES2022 in esbuild builder | +| [8f8e02c32](https://github.com/angular/angular-cli/commit/8f8e02c3221c9477ec931bb6983daf6a2c8dc8be) | fix | support Yarn PNP resolution in modern SASS API | +| [fc82e3bec](https://github.com/angular/angular-cli/commit/fc82e3bec3f188d449e952d9955b845b2efdcd6b) | fix | update browerslist package | +| [0d62157a3](https://github.com/angular/angular-cli/commit/0d62157a30a246c1e00273c2300b9251574e75ae) | fix | update sourcemaps when rebasing Sass url() functions in esbuild builder | +| [1518133db](https://github.com/angular/angular-cli/commit/1518133db3b1c710500786f9f1fcfa05a016862e) | fix | use relative sourcemap source paths for Sass in esbuild builder | +| [fb4ead2ce](https://github.com/angular/angular-cli/commit/fb4ead2ce0de824eef46ce8e27a8f6cc1d08c744) | fix | wait during file watching to improve multi-save rebuilds for esbuild builder | +| [b059fc735](https://github.com/angular/angular-cli/commit/b059fc73597c12330a96fca5f6ab9b1ca226136c) | fix | warn when components styles sourcemaps are not generated when styles optimization is enabled | +| [9d0872fb5](https://github.com/angular/angular-cli/commit/9d0872fb5e369f714633387d9ae39c4242ba1ea1) | perf | add initial global styles incremental rebuilds with esbuild builder | +| [0fe6b3b75](https://github.com/angular/angular-cli/commit/0fe6b3b75b87f6f8050b196615e1c1543b707841) | perf | add vendor chunking to server builder | +| [8c915d414](https://github.com/angular/angular-cli/commit/8c915d41496c99fb42ae3992d9c91de542260bf2) | perf | avoid extra babel file reads in esbuild builder rebuilds | +| [919fe2148](https://github.com/angular/angular-cli/commit/919fe2148885c44655ce36085768b1eab2c8c246) | perf | avoid extra TypeScript emits with esbuild rebuilds | +| [92145c4a7](https://github.com/angular/angular-cli/commit/92145c4a7d2c835b703319676bafd8ea3b4a19f0) | perf | avoid template diagnostics for declaration files in esbuild builder | +| [52db3c000](https://github.com/angular/angular-cli/commit/52db3c00076dfe118cd39d7724229210c30665e0) | perf | minimize Angular diagnostics incremental analysis in esbuild-based builder | +| [feb06753d](https://github.com/angular/angular-cli/commit/feb06753d59f782c6ad8fd59a60537863094f498) | perf | use esbuild-based builder to directly downlevel for await...of | +| [9d83fb91b](https://github.com/angular/angular-cli/commit/9d83fb91b654eed79a5c9c9691d0f1c094f37771) | perf | use Sass worker pool for Sass support in esbuild builder | +| [45a94228f](https://github.com/angular/angular-cli/commit/45a94228fb23acbd0d1a9329448f07b759c8654b) | perf | use Uint8Arrays for incremental caching with esbuild-based builder | +| [f393b0928](https://github.com/angular/angular-cli/commit/f393b09282582da47db683344e037fd1434b32a8) | refactor | disable `requireContext` parsing | +| [12931ba8c](https://github.com/angular/angular-cli/commit/12931ba8c3772b1dd65846cbd6146804b08eab31) | refactor | remove deprecated ES5 support | +| [7f1017e60](https://github.com/angular/angular-cli/commit/7f1017e60f82389568065478d666ae4be6ebfea2) | refactor | remove old `bundleDependencies` enum logic | +| [2ba44a433](https://github.com/angular/angular-cli/commit/2ba44a433c827413a53d12de0ef203f8988ddc2a) | refactor | remove support for Stylus | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [ea4c0aa2e](https://github.com/angular/angular-cli/commit/ea4c0aa2e84d48be37b75e37c99ad381122297c3) | fix | throw error when project has missing root property | +| [de467f46d](https://github.com/angular/angular-cli/commit/de467f46de63059f9c701dfe8695513c742f22b5) | fix | update logger `forEach` `promiseCtor` type | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ | +| [9b07b469b](https://github.com/angular/angular-cli/commit/9b07b469b622e083a9915ed3c24e1d53d8abf38f) | refactor | remove `UpdateBuffer` and rename `UpdateBuffer2` to `UpdateBuffer` | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | +| [43bd0abc1](https://github.com/angular/angular-cli/commit/43bd0abc147cf3177e707624bf6163b3dc9e06f8) | feat | drop support for TypeScript 4.6 and 4.7 | +| [1c1f985b9](https://github.com/angular/angular-cli/commit/1c1f985b9c9913f28915f101ee1717c0da540362) | fix | support inline style sourcemaps when using css-loader for component styles | + +## Special Thanks + +Alan Agius, Brent Schmidt, Charles Lyding, Cédric Exbrayat, Dariusz Ostolski, Doug Parker, Günhan Gülsoy, Jason Bedard, Lukas Spirig, Ruslan Lekhman, angular-robot[bot] and minijus + + + + + +# 14.2.10 (2022-11-17) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------------------------------------- | +| [9ce386caf](https://github.com/angular/angular-cli/commit/9ce386caf6037f21f422a785fec977634406d208) | fix | exclude `@angular/localize@<10.0.0` from ng add pa… ([#24152](https://github.com/angular/angular-cli/pull/24152)) | +| [6446091a3](https://github.com/angular/angular-cli/commit/6446091a310f327ceeb68ae85f3673f6e3e83286) | fix | exclude `@angular/material@7.x` from ng add package discovery | +| [7541e04f3](https://github.com/angular/angular-cli/commit/7541e04f36ff32118e93588be38dcbb5cc2c92a9) | fix | respect registry in RC when running update through yarn | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------- | +| [21cea0b42](https://github.com/angular/angular-cli/commit/21cea0b42f08bf56990bdade82e2daa7c33011ed) | fix | update `loader-utils` to `3.2.1` | + +## Special Thanks + +Alan Agius and Charles Lyding + + + + + +# 13.3.10 (2022-11-17) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------- | +| [f298ebbd5](https://github.com/angular/angular-cli/commit/f298ebbd5f86077985d994662314379df92b6771) | fix | update `loader-utils` to `3.2.1` | + +## Special Thanks + +Alan Agius + + + + + +# 14.2.9 (2022-11-09) + +### @angular-devkit/architect + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [e3e787767](https://github.com/angular/angular-cli/commit/e3e78776782da9d933f7b0e4c6bf391a62585bee) | fix | default to failure if no builder result is provided | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [12b2dc5a2](https://github.com/angular/angular-cli/commit/12b2dc5a2374f992df151af32cc80e2c2d7c4dee) | fix | isolate zone.js usage when rendering server bundles | + +## Special Thanks + +Alan Agius and Charles Lyding + + + + + +# 14.2.8 (2022-11-02) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [4b0ee8ad1](https://github.com/angular/angular-cli/commit/4b0ee8ad15efcb513ab5d9e38bf9b1e08857e798) | fix | guard schematics should include all guards (CanMatch) | + +## Special Thanks + +Andrew Scott + + + + + +# 14.2.7 (2022-10-26) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [91b5bcbb3](https://github.com/angular/angular-cli/commit/91b5bcbb31715a3c2e183e264ebd5ec1188d5437) | fix | disable version check during auto completion | +| [02a3d7b71](https://github.com/angular/angular-cli/commit/02a3d7b715f4069650389ba26a3601747e67d9c2) | fix | skip node.js compatibility checks when running completion | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------- | +| [bebed9df8](https://github.com/angular/angular-cli/commit/bebed9df834d01f72753aa0e60dc104f1781bd67) | fix | issue dev-server support warning when using esbuild builder | + +## Special Thanks + +Alan Agius and Charles Lyding + + + + + +# 14.2.6 (2022-10-12) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------ | +| [1c9cf594f](https://github.com/angular/angular-cli/commit/1c9cf594f7a855ea4b462fad53acd3bf3a2e7622) | fix | handle missing `which` binary in path | +| [28b2cd18e](https://github.com/angular/angular-cli/commit/28b2cd18e3c490cf2db64d4a6744bbd26c0aeabb) | fix | skip downloading temp CLI when running `ng update` without package names | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------- | +| [ad6928184](https://github.com/angular/angular-cli/commit/ad692818413a97afe54aee6a39f0447ee9239343) | fix | project extension warning message should identify concerned project | + +## Special Thanks + +AgentEnder and Alan Agius + + + + + +# 14.2.5 (2022-10-05) + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [17eb20c77](https://github.com/angular/angular-cli/commit/17eb20c77098841d45f0444f5f047c4d44fc614f) | fix | throw more relevant error when Rule returns invalid null value | + +## Special Thanks + +Alan Agius and Charles Lyding + + + + + +# 14.2.4 (2022-09-28) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------- | +| [05b18f4e4](https://github.com/angular/angular-cli/commit/05b18f4e4b39d73c8a3532507c4b7bba8722bf80) | fix | add builders and schematic names as page titles in collected analytics | + +## Special Thanks + +Alan Agius, Jason Bedard and Paul Gschwendtner + + + + + +# 14.2.3 (2022-09-15) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [e7e0cb78f](https://github.com/angular/angular-cli/commit/e7e0cb78f4c6d684fdf25e23a11599b82807cd25) | fix | correctly display error messages that contain "at" text. | +| [4756d7e06](https://github.com/angular/angular-cli/commit/4756d7e0675aa9a8bed11b830b66288141fa6e16) | fix | watch symbolic links | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [1e3ecbdb1](https://github.com/angular/angular-cli/commit/1e3ecbdb138861eff550e05d9662a10d106c0990) | perf | avoid bootstrap conversion AST traversal where possible | + +## Special Thanks + +Alan Agius, Charles Lyding, Jason Bedard and Joey Perrott + + + + + +# 14.2.2 (2022-09-08) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | +| [5405a9b3b](https://github.com/angular/angular-cli/commit/5405a9b3b56675dc671e1ef27410e632f3f6f536) | fix | favor non deprecated packages during update | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [6bfd6a7fb](https://github.com/angular/angular-cli/commit/6bfd6a7fbcaf433bd2c380087803044df4c6d8ee) | fix | update minimum Angular version to 14.2 | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [2b00bca61](https://github.com/angular/angular-cli/commit/2b00bca615a2c79b0a0311c83cb9f1450b6f1745) | fix | allow esbuild-based builder to use SVG Angular templates | +| [45c95e1bf](https://github.com/angular/angular-cli/commit/45c95e1bf1327532ceeb1277fa6f4ce7c3a45581) | fix | change service worker errors to compilation errors | +| [ecc014d66](https://github.com/angular/angular-cli/commit/ecc014d669efe9609177354c465f24a1c94279cd) | fix | handle service-worker serving with localize in dev-server | +| [39ea128c1](https://github.com/angular/angular-cli/commit/39ea128c1294046525a8c098ed6a776407990365) | fix | handling of `@media` queries inside css layers | +| [17b7e1bdf](https://github.com/angular/angular-cli/commit/17b7e1bdfce5823718d1fa915d25858f4b0d7110) | fix | issue warning when using deprecated tilde imports | +| [3afd784f1](https://github.com/angular/angular-cli/commit/3afd784f1f00ee07f68ba112bea7786ccb2d4f35) | fix | watch index file when running build in watch mode | + +## Special Thanks + +Alan Agius, Charles Lyding, Jason Bedard and Joey Perrott + + + + + +# 14.2.1 (2022-08-26) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [e4ca46866](https://github.com/angular/angular-cli/commit/e4ca4686627bd31604cf68bc1d2473337e26864c) | fix | update ng-packagr version to `^14.2.0` | + +## Special Thanks + +Alan Agius + + + + + +# 14.2.0 (2022-08-25) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [596037010](https://github.com/angular/angular-cli/commit/596037010a8113809657cebc9385d040922e6d86) | fix | add missing space after period in warning text | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [44c25511e](https://github.com/angular/angular-cli/commit/44c25511ea2adbd4fbe82a6122fc00af612be8e8) | feat | add ability to serve service worker when using dev-server | +| [3fb569b5c](https://github.com/angular/angular-cli/commit/3fb569b5c82f22afca4dc59313356f198755827e) | feat | switch to Sass modern API in esbuild builder | +| [5bd03353a](https://github.com/angular/angular-cli/commit/5bd03353ac6bb19c983efb7ff015e7aec3ff61d1) | fix | correct esbuild builder global stylesheet sourcemap URL | +| [c4402b1bd](https://github.com/angular/angular-cli/commit/c4402b1bd32cdb0cdd7aeab14239b57ee700d361) | fix | correctly handle parenthesis in url | +| [50c783307](https://github.com/angular/angular-cli/commit/50c783307eb1253f4f2a87502bd7a19f6a409aeb) | fix | use valid CSS comment for sourcemaps with Sass in esbuild builder | +| [4c251853f](https://github.com/angular/angular-cli/commit/4c251853fbc66c6c9aae171dc75612db31afe2fb) | perf | avoid extra string creation with no sourcemaps for esbuild sass | +| [d97640534](https://github.com/angular/angular-cli/commit/d9764053478620a5f4a3349c377c74415435bcbb) | perf | with esbuild builder only load Sass compiler when needed | + +## Special Thanks + +Alan Agius, Charles Lyding, Doug Parker, Jason Bedard, Joey Perrott, Kristiyan Kostadinov and angular-robot[bot] + + + + + +# 14.1.3 (2022-08-17) + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [365035cb3](https://github.com/angular/angular-cli/commit/365035cb37c57e07cb96e45a38f266b16b4e2fbf) | fix | update workspace extension warning to use correct phrasing | + +## Special Thanks + +AgentEnder, Alan Agius, Charles Lyding and Jason Bedard + + + + + +# 14.1.2 (2022-08-10) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------- | +| [3e19c842c](https://github.com/angular/angular-cli/commit/3e19c842cc2a7f2dc62904f5f88025a4687d378a) | fix | avoid collect stats from chunks with no files | +| [d0a0c597c](https://github.com/angular/angular-cli/commit/d0a0c597cd09b1ce4d7134d3e330982b522f28a9) | fix | correctly handle data URIs with escaped quotes in stylesheets | +| [67b3a086f](https://github.com/angular/angular-cli/commit/67b3a086fe90d1b7e5443e8a9f29b12367dd07e7) | fix | process stylesheet resources from url tokens with esbuild browser builder | +| [e6c45c316](https://github.com/angular/angular-cli/commit/e6c45c316ebcd1b5a16b410a3743088e9e9f789c) | perf | reduce babel transformation in esbuild builder | +| [38b71bcc0](https://github.com/angular/angular-cli/commit/38b71bcc0ddca1a34a5a4480ecd0b170bd1e9620) | perf | use esbuild in esbuild builder to downlevel native async/await | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [dd47a5e8c](https://github.com/angular/angular-cli/commit/dd47a5e8c543cbd3bb37afe5040a72531b028347) | fix | elide type only named imports when using `emitDecoratorMetadata` | + +## Special Thanks + +Alan Agius, Charles Lyding and Jason Bedard + + + + + +# 14.1.1 (2022-08-03) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [4ee825bac](https://github.com/angular/angular-cli/commit/4ee825baca21c21db844bdf718b6ec29dc6c3d42) | fix | catch clause variable is not an Error instance | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------- | +| [83dcfb32f](https://github.com/angular/angular-cli/commit/83dcfb32f8ef3334f83bb36a2c3097fe9f8a4e4b) | fix | prevent numbers from class names | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------- | +| [ef6da4aad](https://github.com/angular/angular-cli/commit/ef6da4aad76ff534d4edb9e73c2d56c53b649b15) | fix | allow the esbuild-based builder to fully resolve global stylesheet packages | +| [eed54b359](https://github.com/angular/angular-cli/commit/eed54b359d2b514156242529ee8a25b51c50dae0) | fix | catch clause variable is not an Error instance | +| [c98471094](https://github.com/angular/angular-cli/commit/c9847109438d33d38a31ded20a1cab2721fc1fbd) | fix | correctly respond to preflight requests | +| [94b444e4c](https://github.com/angular/angular-cli/commit/94b444e4caff4c3092e0291d9109e2abed966656) | fix | correctly set `ngDevMode` in esbuilder | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------- | +| [44c18082a](https://github.com/angular/angular-cli/commit/44c18082a5963b7f9d0f1577a0975b2f35abe6a2) | fix | `classify` string util should concat string without using a `.` | + +### @angular/create + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [cb0d3fb33](https://github.com/angular/angular-cli/commit/cb0d3fb33f196393761924731c3c3786a3a3493b) | fix | use appropriate package manager to install dependencies | + +## Special Thanks + +Alan Agius, Charles Lyding, Jason Bedard and Paul Gschwendtner + + + + + +# 14.1.0 (2022-07-20) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [3884b8652](https://github.com/angular/angular-cli/commit/3884b865262c1ffa5652ac0f4d67bbf59087f453) | fix | add esbuild browser builder to workspace schema | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [707911d42](https://github.com/angular/angular-cli/commit/707911d423873623d4201d2fbce4a294ab73a135) | feat | support controlling `addDependency` utility rule install behavior | +| [a8fe4fcc3](https://github.com/angular/angular-cli/commit/a8fe4fcc315fd408b5b530a44a02c1655b5450a8) | fix | Allow skipping existing dependencies in E2E schematic | +| [b8bf3b480](https://github.com/angular/angular-cli/commit/b8bf3b480bef752641370e542ebb5aee649a8ac6) | fix | only issue a warning for addDependency existing specifier | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------ | +| [a7709b718](https://github.com/angular/angular-cli/commit/a7709b718c953d83f3bde00fa3bf896501359946) | feat | add `externalDependencies` to the esbuild browser builder | +| [248860ad6](https://github.com/angular/angular-cli/commit/248860ad674b54f750bb5c197588bb6d031be208) | feat | add Sass file support to experimental esbuild-based builder | +| [b06ae5514](https://github.com/angular/angular-cli/commit/b06ae55140c01f8b5107527fd0af1da3b04a721f) | feat | add service worker support to experimental esbuild builder | +| [b5f6d862b](https://github.com/angular/angular-cli/commit/b5f6d862b95afd0ec42d9b3968e963f59b1b1658) | feat | Identify third-party sources in sourcemaps | +| [b3a14d056](https://github.com/angular/angular-cli/commit/b3a14d05629ba6e3b23c09b1bfdbc4b35d534813) | fix | allow third-party sourcemaps to be ignored in esbuild builder | +| [53dd929e5](https://github.com/angular/angular-cli/commit/53dd929e59f98a7088d150e861d18e97e6de4114) | fix | ensure esbuild builder sourcemap sources are relative to workspace | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [526cdb263](https://github.com/angular/angular-cli/commit/526cdb263a8c74ad228f584f70dc029aa69351d7) | feat | allow `chain` rule to accept iterables of rules | + +### @angular/create + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------- | +| [cfe93fbc8](https://github.com/angular/angular-cli/commit/cfe93fbc89fad2f58826f0118ce7ff421cd0e4f2) | feat | add support for `yarn create` and `npm init` | + +## Special Thanks + +Alan Agius, Charles Lyding, Derek Cormier, Doug Parker, Jason Bedard, Joey Perrott, Paul Gschwendtner, Victor Porof and renovate[bot] + + + + + +# 14.0.7 (2022-07-20) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------- | +| [f653bf4fb](https://github.com/angular/angular-cli/commit/f653bf4fbb69b9e0fa0e6440a88a30f17566d9a3) | fix | incorrect logo for Angular Material | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------- | +| [5810c2cc2](https://github.com/angular/angular-cli/commit/5810c2cc2dd21e5922a5eaa330e854e4327a0500) | fix | fallback to use projectRoot when sourceRoot is missing during coverage | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------- | +| [2ba4678b6](https://github.com/angular/angular-cli/commit/2ba4678b6ba2164e80cb661758565c133e08afaa) | fix | add i18n as valid project extension | +| [c2201c835](https://github.com/angular/angular-cli/commit/c2201c835801ef9c1cc6cacec2748c8ca341519d) | fix | log name of invalid extension too | + +## Special Thanks + +Alan Agius, Fortunato Ventre, Katerina Skroumpelou and Kristiyan Kostadinov + + + + + +# 13.3.9 (2022-07-20) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------- | +| [0d62716ae](https://github.com/angular/angular-cli/commit/0d62716ae3753bb463de6b176ae07520ebb24fc9) | fix | update terser to address CVE-2022-25858 | + +## Special Thanks + +Alan Agius and Charles Lyding + + + + + +# 14.0.6 (2022-07-13) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------- | +| [178550529](https://github.com/angular/angular-cli/commit/1785505290940dad2ef9a62d4725e0d1b4b486d4) | fix | handle cases when completion is enabled and running in an older CLI workspace | +| [10f24498e](https://github.com/angular/angular-cli/commit/10f24498ec2938487ae80d6ecea584e20b01dcbe) | fix | remove deprecation warning of `no` prefixed schema options | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------- | +| [dfa6d73c5](https://github.com/angular/angular-cli/commit/dfa6d73c5c45d3c3276fb1fecfb6535362d180c5) | fix | remove browserslist configuration | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------- | +| [4d848c4e6](https://github.com/angular/angular-cli/commit/4d848c4e6f6944f32b9ecb2cf2db5c544b3894fe) | fix | generate different content hashes for scripts which are changed during the optimization phase | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- | +| [2500f34a4](https://github.com/angular/angular-cli/commit/2500f34a401c2ffb03b1dfa41299d91ddebe787e) | fix | provide actionable warning when a workspace project has missing `root` property | + +## Special Thanks + +Alan Agius and martinfrancois + + + + + +# 14.0.5 (2022-07-06) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------ | +| [98a6aad60](https://github.com/angular/angular-cli/commit/98a6aad60276960bd6bcecda73172480e4bdec48) | fix | during an update only use package manager force option with npm 7+ | +| [094aa16aa](https://github.com/angular/angular-cli/commit/094aa16aaf5b148f2ca94cae45e18dbdeaacad9d) | fix | improve error message for project-specific ng commands when run outside of a project | +| [e5e07fff1](https://github.com/angular/angular-cli/commit/e5e07fff1919c46c15d6ce61355e0c63007b7d55) | fix | show deprecated workspace config options in IDE | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [f9f970cab](https://github.com/angular/angular-cli/commit/f9f970cab515a8a1b1fbb56830b03250dd5cccce) | fix | prevent importing `RouterModule` parallel to `RoutingModule` | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------- | +| [aa8ed532f](https://github.com/angular/angular-cli/commit/aa8ed532f816f2fa23b1fe443a216c5d75507432) | fix | disable glob mounting for patterns that start with a forward slash | +| [c76edb8a7](https://github.com/angular/angular-cli/commit/c76edb8a79d1a12376c2a163287251c06e1f0222) | fix | don't override base-href in HTML when it's not set in builder | +| [f64903528](https://github.com/angular/angular-cli/commit/f649035286d640660c3bc808b7297fb60d0888bc) | fix | improve detection of CommonJS dependencies | +| [74dbd5fc2](https://github.com/angular/angular-cli/commit/74dbd5fc273aece097b2b3ee0b28607d24479d8c) | fix | support hidden component stylesheet sourcemaps with esbuild builder | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [7aed97561](https://github.com/angular/angular-cli/commit/7aed97561c2320f92f8af584cc9852d4c8d818b9) | fix | do not run ngcc when `node_modules` does not exist | + +## Special Thanks + +Alan Agius, Charles Lyding, JoostK and Paul Gschwendtner + + + + + +# 14.0.4 (2022-06-29) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [fc72c625b](https://github.com/angular/angular-cli/commit/fc72c625bb7db7b9c8d865086bcff05e2db426ee) | fix | correctly handle `--collection` option in `ng new` | +| [f5badf221](https://github.com/angular/angular-cli/commit/f5badf221d2a2f5357f93bf0e32146669f8bbede) | fix | improve global schema validation | +| [ed302ea4c](https://github.com/angular/angular-cli/commit/ed302ea4c80b4f6fe8a73c5a0d25055a7dca1db2) | fix | remove color from help epilogue | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [c58c66c0d](https://github.com/angular/angular-cli/commit/c58c66c0d5c76630453151b65b1a1c3707c82e9f) | fix | use `sourceRoot` instead of `src` in universal schematic | + +### @angular-devkit/architect + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------- | +| [88acec1fd](https://github.com/angular/angular-cli/commit/88acec1fd302d7d8a053e37ed0334ec6a30c952c) | fix | complete builders on the next event loop iteration | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------- | +| [694b73dfa](https://github.com/angular/angular-cli/commit/694b73dfa12e5aefff8fc5fdecf220833ac40b42) | fix | exit dev-server when CTRL+C is pressed | +| [6d4782199](https://github.com/angular/angular-cli/commit/6d4782199c4a4e92a9c0b189d6a7857ca631dd3f) | fix | exit localized builds when CTRL+C is pressed | +| [282baffed](https://github.com/angular/angular-cli/commit/282baffed507926e806db673b6804b9299c383af) | fix | hide stacktraces from webpack errors | +| [c4b0abf5b](https://github.com/angular/angular-cli/commit/c4b0abf5b8c1e392ead84c8810e8d6e615fd0024) | fix | set base-href in service worker manifest when using i18n and app-shell | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [33f1cc192](https://github.com/angular/angular-cli/commit/33f1cc192d963b4a4348bb41b8fb0969ffd5c342) | fix | restore process title after NGCC is executed | +| [6796998bf](https://github.com/angular/angular-cli/commit/6796998bf4dd829f9ac085a52ce7e9d2cda73fd1) | fix | show a compilation error on invalid TypeScript version | + +## Special Thanks + +Alan Agius, Charles Lyding and Tim Bowersox + + + + + +# 14.0.3 (2022-06-23) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------- | +| [b3db91baf](https://github.com/angular/angular-cli/commit/b3db91baf50c92589549a66ffef437f7890d3de7) | fix | disable version check when running `ng completion` commands | +| [cdab9fa74](https://github.com/angular/angular-cli/commit/cdab9fa7431db7e2a75e04e776555b8e5e15fc94) | fix | provide an actionable error when using `--configuration` with `ng run` | +| [5521648e3](https://github.com/angular/angular-cli/commit/5521648e33af634285f6352b43a324a1ee023e27) | fix | temporarily handle boolean options in schema prefixed with `no` | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [5e960ce24](https://github.com/angular/angular-cli/commit/5e960ce246e7090f57ce22723911a743aa8fcb0c) | fix | fix incorrect glob cwd in karma when using `--include` option | +| [1b5e92075](https://github.com/angular/angular-cli/commit/1b5e92075e64563459942d4de785f1a8bef46ec7) | fix | handle `codeCoverageExclude` correctly in Windows | +| [ff6d81a45](https://github.com/angular/angular-cli/commit/ff6d81a4539657446c8f5770cefe688d2d578450) | fix | ignore supported browsers during i18n extraction | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [170c16f2e](https://github.com/angular/angular-cli/commit/170c16f2ea769e76a48f1ac215ee88ba47ff511d) | fix | workspace writer skip creating empty projects property | + +## Special Thanks + +Alan Agius, Charles Lyding and Paul Gschwendtner + + + + + +# 14.0.2 (2022-06-15) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [23095e9c3](https://github.com/angular/angular-cli/commit/23095e9c3fc514c7e9a892833d8a18270da5bd95) | fix | show more actionable error when command is ran in wrong scope | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [5a486cb64](https://github.com/angular/angular-cli/commit/5a486cb64253ba2829160a6f1fa3bf0e381d45ea) | fix | remove vscode testing configurations for `minimal` workspaces | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------- | +| [9d88c96d8](https://github.com/angular/angular-cli/commit/9d88c96d898c5c46575a910a7230d239f4fe7a77) | fix | replace fallback locale for `en-US` | + +## Special Thanks + +Alan Agius and Julien Marcou + + + + + +# 13.3.8 (2022-06-15) + +### @angular/pwa + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------- | +| [c7f994f88](https://github.com/angular/angular-cli/commit/c7f994f88a396be96c01da1017a15083d5f544fb) | fix | add peer dependency on Angular CLI | + +## Special Thanks + +Alan Agius + + + + + +# 14.0.1 (2022-06-08) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------------------------------------- | +| [e4fb96657](https://github.com/angular/angular-cli/commit/e4fb96657f044d97562008b5b3c6f3a55ac8ba3a) | fix | add text to help output to indicate that additional commands are available when ran in different context | +| [7952e5790](https://github.com/angular/angular-cli/commit/7952e579066f7191f4b82a10816c6a41a4ea5644) | fix | avoid creating unnecessary global configuration | +| [66a1d6b9d](https://github.com/angular/angular-cli/commit/66a1d6b9d2e1fba3d5ee88a6c5d81206f530ce3a) | fix | correct scope cache command | +| [e2d964289](https://github.com/angular/angular-cli/commit/e2d964289fe2a418e5f4e421249e2f8da64185cc) | fix | correctly print package manager name when an install is needed | +| [75fd3330d](https://github.com/angular/angular-cli/commit/75fd3330d4c27263522ea931eb1545ce0a34ab6a) | fix | during an update only use package manager force option with npm 7+ | +| [e223890c1](https://github.com/angular/angular-cli/commit/e223890c1235b4564ec15eb99d71256791a21c3c) | fix | ensure full process exit with older local CLI versions | +| [0cca3638a](https://github.com/angular/angular-cli/commit/0cca3638adb46cd5d0c18b823c83d4b604d7c798) | fix | handle project being passed as a flag | +| [b1451cb5e](https://github.com/angular/angular-cli/commit/b1451cb5e90f43df365202a6fdfcfbc9e0853ca4) | fix | improve resilience of logging during process exit | +| [17fec1357](https://github.com/angular/angular-cli/commit/17fec13577ac333fc66c3752c75be58146c9ebac) | fix | provide actionable error when project cannot be determined | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------- | +| [73dcf39c6](https://github.com/angular/angular-cli/commit/73dcf39c6e7678a3915a113fd72829549ccc3b8e) | fix | remove strict setting under application project | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [c788d5b56](https://github.com/angular/angular-cli/commit/c788d5b56a1a191e7ca53c3b63245e3979a1cf44) | fix | log modified and removed files when using the `verbose` option | +| [6e8fe0ed5](https://github.com/angular/angular-cli/commit/6e8fe0ed54d88132da0238fdb3a6e97330c85ff7) | fix | replace dev-server socket path from `/ws` to `/ng-cli-ws` | +| [651adadf4](https://github.com/angular/angular-cli/commit/651adadf4df8b66c60771f27737cb2a67957b46a) | fix | update Angular peer dependencies to 14.0 stable | + +### @angular/pwa + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------- | +| [cfd264d06](https://github.com/angular/angular-cli/commit/cfd264d061109c7989933e51a14b6bf83b289b07) | fix | add peer dependency on Angular CLI | + +## Special Thanks + +Alan Agius, Charles Lyding and Doug Parker + + + + + +# 14.0.0 (2022-06-02) + +## Breaking Changes + +### @angular/cli + +- Several changes to the `ng analytics` command syntax. + + - `ng analytics project ` has been replaced with `ng analytics ` + - `ng analytics ` has been replaced with `ng analytics --global` + +- Support for Node.js v12 has been removed as it will become EOL on 2022-04-30. Please use Node.js v14.15 or later. +- Support for TypeScript 4.4 and 4.5 has been removed. Please update to TypeScript 4.6. +- `--all` option from `ng update` has been removed without replacement. To update packages which don’t provide `ng update` capabilities in your workspace `package.json` use `npm update`, `yarn upgrade-interactive` or `yarn upgrade` instead. +- Deprecated option `--prod` has been removed from all builders. `--configuration production`/`-c production` should be used instead if the default configuration of the builder is not configured to `production`. +- `--configuration` cannot be used with `ng run`. Provide the configuration as part of the target. Ex: `ng run project:builder:configuration`. +- Deprecated `ng x18n` and `ng i18n-extract` commands have been removed in favor of `ng extract-i18n`. +- Several changes in the Angular CLI commands and arguments handling. + + - `ng help` has been removed in favour of the `—-help` option. + - `ng —-version` has been removed in favour of `ng version` and `ng v`. + - Deprecated camel cased arguments are no longer supported. Ex. using `—-sourceMap` instead of `—-source-map` will result in an error. + - `ng update`, `—-migrate-only` option no longer accepts a string of migration name, instead use `—-migrate-only -—name `. + - `—-help json` help has been removed. + +### @angular-devkit/architect-cli + +- camel case arguments are no longer allowed. + +### @angular-devkit/schematics-cli + +- camel case arguments are no longer allowed. + +### @angular-devkit/build-angular + +- `browser` and `karma` builders `script` and `styles` options input files extensions are now validated. + + Valid extensions for `scripts` are: + + - `.js` + - `.cjs` + - `.mjs` + - `.jsx` + - `.cjsx` + - `.mjsx` + + Valid extensions for `styles` are: + + - `.css` + - `.less` + - `.sass` + - `.scss` + - `.styl` + +- We now issue a build time error since importing a CSS file as an ECMA module is non standard Webpack specific feature, which is not supported by the Angular CLI. + + This feature was never truly supported by the Angular CLI, but has as such for visibility. + +- Reflect metadata polyfill is no longer automatically provided in JIT mode + Reflect metadata support is not required by Angular in JIT applications compiled by the CLI. + Applications built in AOT mode did not and will continue to not provide the polyfill. + For the majority of applications, the reflect metadata polyfill removal should have no effect. + However, if an application uses JIT mode and also uses the previously polyfilled reflect metadata JavaScript APIs, the polyfill will need to be manually added to the application after updating. + To replicate the previous behavior, the `core-js` package should be manually installed and the `import 'core-js/proposals/reflect-metadata';` statement should be added to the application's `polyfills.ts` file. +- `NG_BUILD_CACHE` environment variable has been removed. `cli.cache` in the workspace configuration should be used instead. +- The deprecated `showCircularDependencies` browser and server builder option has been removed. The recommended method to detect circular dependencies in project code is to use either a lint rule or other external tools. + +### @angular-devkit/core + +- `parseJson` and `ParseJsonOptions` APIs have been removed in favor of 3rd party JSON parsers such as `jsonc-parser`. +- The below APIs have been removed without replacement. Users should leverage other Node.js or other APIs. + - `fs` namespace + - `clean` + - `mapObject` + +### @angular-devkit/schematics + +- Schematics `NodePackageInstallTask` will not execute package scripts by default + The `NodePackageInstallTask` will now use the package manager's `--ignore-scripts` option by default. + The `--ignore-scripts` option will prevent package scripts from executing automatically during an install. + If a schematic installs packages that need their `install`/`postinstall` scripts to be executed, the + `NodePackageInstallTask` now contains an `allowScripts` boolean option which can be enabled to provide the + previous behavior for that individual task. As with previous behavior, the `allowScripts` option will + prevent the individual task's usage of the `--ignore-scripts` option but will not override the package + manager's existing configuration. +- Deprecated `analytics` property has been removed from `TypedSchematicContext` interface + +### @ngtools/webpack + +- `ivy` namespace has been removed from the public API. + + - `ivy.AngularWebpackPlugin` -> `AngularWebpackPlugin` + - `ivy.AngularPluginOptions` -> `AngularPluginOptions` + +## Deprecations + +### @angular/cli + +- The `defaultCollection` workspace option has been deprecated in favor of `schematicCollections`. + + Before + + ```json + "defaultCollection": "@angular/material" + ``` + + After + + ```json + "schematicCollections": ["@angular/material"] + ``` + +- The `defaultProject` workspace option has been deprecated. The project to use will be determined from the current working directory. + +### @angular-devkit/core + +- - `ContentHasMutatedException`, `InvalidUpdateRecordException`, `UnimplementedException` and `MergeConflictException` symbol from `@angular-devkit/core` have been deprecated in favor of the symbol from `@angular-devkit/schematics`. + - `UnsupportedPlatformException` - A custom error exception should be created instead. + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------- | +| [afafa5788](https://github.com/angular/angular-cli/commit/afafa5788f11b8727c39bb0a390300a706aba5bc) | feat | add `--global` option to `ng analytics` command | +| [bb550436a](https://github.com/angular/angular-cli/commit/bb550436a476d74705742a8c36f38971b346b903) | feat | add `ng analytics info` command | +| [e5bf35ea3](https://github.com/angular/angular-cli/commit/e5bf35ea3061a3e532aa85df44551107e62e24c5) | feat | add `ng cache` command | +| [7ab22ed40](https://github.com/angular/angular-cli/commit/7ab22ed40d521e3cec29ab2d66d0289c3cdb4106) | feat | add disable/enable aliases for off/on `ng analytics` command | +| [4212fb8de](https://github.com/angular/angular-cli/commit/4212fb8de2f4f3e80831a0803acc5fc6e54db1e1) | feat | add prompt to set up CLI autocompletion | +| [0316dea67](https://github.com/angular/angular-cli/commit/0316dea676be522b04d654054880cc5794e3c8b3) | feat | add prompts on missing builder targets | +| [607a723f7](https://github.com/angular/angular-cli/commit/607a723f7d623ec8a15054722b2afd13042f66a1) | feat | add support for auto completion | +| [366cabc66](https://github.com/angular/angular-cli/commit/366cabc66c3dd836e2fdfea8dad6c4c7c2096b1d) | feat | add support for multiple schematics collections | +| [036327e9c](https://github.com/angular/angular-cli/commit/036327e9ca838f9ef3f117fbd18949d9d357e68d) | feat | deprecated `defaultProject` option | +| [fb0622893](https://github.com/angular/angular-cli/commit/fb06228932299870774a7b254f022573f5d8175f) | feat | don't prompt to set up autocompletion for `ng update` and `ng completion` commands | +| [4ebfe0341](https://github.com/angular/angular-cli/commit/4ebfe03415ebe4e8f1625286d1be8bd1b54d3862) | feat | drop support for Node.js 12 | +| [022d8c7bb](https://github.com/angular/angular-cli/commit/022d8c7bb142e8b83f9805a39bc1ae312da465eb) | feat | make `ng completion` set up CLI autocompletion by modifying `.bashrc` files | +| [2e15df941](https://github.com/angular/angular-cli/commit/2e15df9417dcc47b12785a8c4c9074bf05d0450c) | feat | remember after prompting users to set up autocompletion and don't prompt again | +| [7fa3e6587](https://github.com/angular/angular-cli/commit/7fa3e6587955d0638929758d3c257392c242c796) | feat | support TypeScript 4.6.2 | +| [9e69331fa](https://github.com/angular/angular-cli/commit/9e69331fa61265c77d6281232bb64a2c63509290) | feat | use PNPM as package manager when `pnpm-lock.yaml` exists | +| [6f6b453fb](https://github.com/angular/angular-cli/commit/6f6b453fbf90adad16eba7ea8929a11235c1061b) | fix | `ng doc` doesn't open browser in Windows | +| [8e66c9188](https://github.com/angular/angular-cli/commit/8e66c9188be827380e5acda93c7e21fae718b9ce) | fix | `ng g` show description from `collection.json` if not present in `schema.json` | +| [9edeb8614](https://github.com/angular/angular-cli/commit/9edeb86146131878c5e8b21b6adaa24a26f12453) | fix | add long description to `ng update` | +| [160cb0718](https://github.com/angular/angular-cli/commit/160cb071870602d9e7fece2ce381facb71e7d762) | fix | correctly handle `--search` option in `ng doc` | +| [d46cf6744](https://github.com/angular/angular-cli/commit/d46cf6744eadb70008df1ef25e24fb1db58bb997) | fix | display option descriptions during auto completion | +| [09f8659ce](https://github.com/angular/angular-cli/commit/09f8659cedcba70903140d0c3eb5d0e10ebb506c) | fix | display package manager during `ng update` | +| [a49cdfbfe](https://github.com/angular/angular-cli/commit/a49cdfbfefbdd756882be96fb61dc8a0d374b6e0) | fix | don't prompt for analytics when running `ng analytics` | +| [4b22593c4](https://github.com/angular/angular-cli/commit/4b22593c4a269ea4bd63cef39009aad69f159fa1) | fix | ensure all available package migrations are executed | +| [054ae02c2](https://github.com/angular/angular-cli/commit/054ae02c2fb8eed52af76cf39a432a3770d301e4) | fix | favor project in cwd when running architect commands | +| [ff4eba3d4](https://github.com/angular/angular-cli/commit/ff4eba3d4a9417d2baef70aaa953bdef4bb426a6) | fix | handle duplicate arguments | +| [5a8bdeb43](https://github.com/angular/angular-cli/commit/5a8bdeb434c7561334bfc8865ed279110a44bd93) | fix | hide private schematics from `ng g` help output | +| [644f86d55](https://github.com/angular/angular-cli/commit/644f86d55b75a289e641ba280e8456be82383b06) | fix | improve error message for Windows autocompletion use cases | +| [3012036e8](https://github.com/angular/angular-cli/commit/3012036e81fc6e5fc6c0f1df7ec626f91285673e) | fix | populate path with working directory in nested schematics | +| [8a396de6a](https://github.com/angular/angular-cli/commit/8a396de6a8a58347d2201a43d7f5101f94f20e89) | fix | print entire config when no positional args are provided to `ng config` | +| [bdf2b9bfa](https://github.com/angular/angular-cli/commit/bdf2b9bfa9893a940ba254073d024172e0dc1abc) | fix | print schematic errors correctly | +| [efc3c3225](https://github.com/angular/angular-cli/commit/efc3c32257a65caf36999dc34cadc41eedcbf323) | fix | remove analytics prompt postinstall script | +| [bf15b202b](https://github.com/angular/angular-cli/commit/bf15b202bb1cd073fe01cf387dce2c033b5bb14c) | fix | remove cache path from global valid paths | +| [142da460b](https://github.com/angular/angular-cli/commit/142da460b22e07a5a37b6140b50663446c3a2dbf) | fix | remove incorrect warning during `ng update` | +| [96a0d92da](https://github.com/angular/angular-cli/commit/96a0d92da2903edfb3835ce86b3700629d6e43ad) | fix | remove JSON serialized description from help output | +| [78460e995](https://github.com/angular/angular-cli/commit/78460e995a192336db3c4be9d0592b4e7a2ff2c8) | fix | remove type casting and add optional chaining for current in optionTransforms | +| [e5bdadac4](https://github.com/angular/angular-cli/commit/e5bdadac44ac023363bc0a2473892fc17430b81f) | fix | skip prompt or warn when setting up autocompletion without a global CLI install | +| [ca401255f](https://github.com/angular/angular-cli/commit/ca401255f49568cfe5f9ec6a35ea5b91c91afa70) | fix | sort commands in help output | +| [b97772dfc](https://github.com/angular/angular-cli/commit/b97772dfc03401fe1faa79e77742905341bd5d46) | fix | support silent package installs with Yarn 2+ | +| [87cd5cd43](https://github.com/angular/angular-cli/commit/87cd5cd4311e71a15ea1ecb82dde7480036cb815) | fix | workaround npm 7+ peer dependency resolve errors during updates | +| [d94a67353](https://github.com/angular/angular-cli/commit/d94a67353dcdaa30cf5487744a7ef151a6268f2d) | refactor | remove deprecated `--all` option from `ng update` | +| [2fc7c73d7](https://github.com/angular/angular-cli/commit/2fc7c73d7e40dbb0a593df61eeba17c8a8f618a9) | refactor | remove deprecated `--prod` flag | +| [b69ca3a7d](https://github.com/angular/angular-cli/commit/b69ca3a7d22b54fc06fbc1cfb559b2fd915f5609) | refactor | remove deprecated command aliases for `extract-i18n`. | +| [2e0493130](https://github.com/angular/angular-cli/commit/2e0493130acfe7244f7ee3ef28c961b1b04d7722) | refactor | replace command line arguments parser | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------- | +| [7b78b7840](https://github.com/angular/angular-cli/commit/7b78b7840e95b0f4dca2fcb9218b67dd7500ff2c) | feat | add --standalone to ng generate | +| [e49220fba](https://github.com/angular/angular-cli/commit/e49220fba0d158be0971989e26eb199ec02fa113) | feat | add migratiom to remove `defaultProject` in workspace config | +| [3fa38b08b](https://github.com/angular/angular-cli/commit/3fa38b08ba8ef57a6079873223a7d6088d5ea64e) | feat | introduce `addDependency` rule to utilities | +| [b07ccfbb1](https://github.com/angular/angular-cli/commit/b07ccfbb1b2045d285c23dd4b654e1380892fcb2) | feat | introduce a utility subpath export for Angular rules and utilities | +| [7e7de6858](https://github.com/angular/angular-cli/commit/7e7de6858dd71bd461ceb0f89e29e2c57099bbcc) | feat | update Angular dependencies to use `^` as version prefix | +| [69ecddaa7](https://github.com/angular/angular-cli/commit/69ecddaa7d8b01aa7a9e61c403a4b9a8669e34c4) | feat | update new and existing projects compilation target to `ES2020` | +| [7e8e42063](https://github.com/angular/angular-cli/commit/7e8e42063f354c402d758f10c8ba9bee7e0c8aff) | fix | add migration to remove `package.json` in libraries secondary entrypoints | +| [b928d973e](https://github.com/angular/angular-cli/commit/b928d973e97f33220afe16549b41c4031feb5c5e) | fix | alphabetically order imports during component generation | +| [09a71bab6](https://github.com/angular/angular-cli/commit/09a71bab6044e517319f061dbd4555ce57fe6485) | fix | Consolidated setup with a single `beforeEach()` | +| [1921b07ee](https://github.com/angular/angular-cli/commit/1921b07eeb710875825dc6f7a4452bd5462e6ba7) | fix | don't add path mapping to old entrypoint definition file | +| [c927c038b](https://github.com/angular/angular-cli/commit/c927c038ba356732327a026fe9a4c36ed23c9dec) | fix | remove `@types/node` from new projects | +| [27cb29438](https://github.com/angular/angular-cli/commit/27cb29438aa01b185b2dca3617100d87f45f14e8) | fix | remove extra space in standalone imports | + +### @angular-devkit/architect-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | -------------------------------- | +| [c7556b62b](https://github.com/angular/angular-cli/commit/c7556b62b7b0eab5717ed6eeab3fa7f0f1f2a873) | refactor | replace parser with yargs-parser | + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | -------------------------------- | +| [5330d52ae](https://github.com/angular/angular-cli/commit/5330d52aee32daca27fa1a2fa15712f4a408602a) | refactor | replace parser with yargs-parser | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------ | +| [00186fb93](https://github.com/angular/angular-cli/commit/00186fb93f66d8da51886de37cfa4599f3e89af9) | feat | add initial experimental esbuild-based application browser builder | +| [d23a168b8](https://github.com/angular/angular-cli/commit/d23a168b8d558ae9d73c8c9eed4ff199fc4d74b9) | feat | validate file extensions for `scripts` and `styles` options | +| [2adf252dc](https://github.com/angular/angular-cli/commit/2adf252dc8a7eb0ce504de771facca56730e5272) | fix | add es2015 exports package condition to browser-esbuild | +| [72e820e7b](https://github.com/angular/angular-cli/commit/72e820e7b2bc6904b030f1092bbb610334a4036f) | fix | better handle Windows paths in esbuild experimental builder | +| [587082fb0](https://github.com/angular/angular-cli/commit/587082fb0fa7bdb6cddb36327f791889d76e3e7b) | fix | close compiler on Karma exit | +| [c52d10d1f](https://github.com/angular/angular-cli/commit/c52d10d1fc4b70483a2043edfa73dc0f323f6bf1) | fix | close dev-server on error | +| [48630ccfd](https://github.com/angular/angular-cli/commit/48630ccfd7a672fc5174ef484b3bd5c549d32fef) | fix | detect `tailwind.config.cjs` as valid tailwindcss configuration | +| [4d5f6c659](https://github.com/angular/angular-cli/commit/4d5f6c65918c1a8a4bde0a0af01089242d1cdc4a) | fix | downlevel libraries based on the browserslist configurations | +| [1a160dac0](https://github.com/angular/angular-cli/commit/1a160dac00f34aab089053281c640dba3efd597f) | fix | ensure karma sourcemap support on Windows | +| [07e776ea3](https://github.com/angular/angular-cli/commit/07e776ea379a50a98a50cf590156c2dc1b272e78) | fix | fail build when importing CSS files as an ECMA modules | +| [ac1383f9e](https://github.com/angular/angular-cli/commit/ac1383f9e5d491181812c090bd4323f46110f3d8) | fix | properly handle locally-built APF v14 libraries | +| [966d25b55](https://github.com/angular/angular-cli/commit/966d25b55eeb6cb84eaca183b30e7d3b0d0a2188) | fix | remove unneeded JIT reflect metadata polyfill | +| [b8564a638](https://github.com/angular/angular-cli/commit/b8564a638df3b6971ef2ac8fb838e6a7c910ac3b) | refactor | remove deprecated `NG_BUILD_CACHE` environment variable | +| [0a1cd584d](https://github.com/angular/angular-cli/commit/0a1cd584d8ed00889b177f4284baec7e5427caf2) | refactor | remove deprecated `showCircularDependencies` browser and server builder option | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------- | +| [c5b3e9299](https://github.com/angular/angular-cli/commit/c5b3e9299130132aecfa19219405e1964d0c5443) | refactor | deprecate unused exception classes | +| [67144b9e5](https://github.com/angular/angular-cli/commit/67144b9e54b5a9bfbc963e386b01275be5eaccf5) | refactor | remove deprecated `parseJson` and `ParseJsonOptions` APIs | +| [a0c02af7e](https://github.com/angular/angular-cli/commit/a0c02af7e340bb16f4e6f523c2d835c9b18926b3) | refactor | remove deprecated fs, object and array APIs | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------- | +| [c9c781c7d](https://github.com/angular/angular-cli/commit/c9c781c7d5f3c6de780912fd7c624a457e6da14c) | feat | add parameter to `listSchematicNames` to allow returning hidden schematics. | +| [0e6425fd8](https://github.com/angular/angular-cli/commit/0e6425fd88ea32679516251efdca6ff07cc4b56a) | feat | disable package script execution by default in `NodePackageInstallTask` | +| [25498ad5b](https://github.com/angular/angular-cli/commit/25498ad5b2ba6fa5a88c9802ddeb0ed85c5d9b60) | feat | re-export core string helpers from schematics package | +| [464cf330a](https://github.com/angular/angular-cli/commit/464cf330a14397470e1e57450a77f421a45a927e) | feat | support null for options parameter from OptionTransform type | +| [33f9f3de8](https://github.com/angular/angular-cli/commit/33f9f3de869bba2ecd855a01cc9a0a36651bd281) | feat | support reading JSON content directly from a Tree | +| [01297f450](https://github.com/angular/angular-cli/commit/01297f450387dea02eafd6f5701c417ab5c5d844) | feat | support reading text content directly from a Tree | +| [48f9b79bc](https://github.com/angular/angular-cli/commit/48f9b79bc4d43d0180bab5af5726621a68204a15) | fix | support ignore scripts package installs with Yarn 2+ | +| [3471cd6d8](https://github.com/angular/angular-cli/commit/3471cd6d8696ae9c28dba901d3e0f6868d69efc8) | fix | support quiet package installs with Yarn 2+ | +| [44c1e6d0d](https://github.com/angular/angular-cli/commit/44c1e6d0d2db5f2dc212d63a34ade045cb7854d5) | refactor | remove deprecated `analytics` property | + +### @angular/pwa + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [243cb4062](https://github.com/angular/angular-cli/commit/243cb40622fef4107b0162bc7b6a374471cebc14) | fix | remove `@schematics/angular` utility deep import usage | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------ | +| [0c344259d](https://github.com/angular/angular-cli/commit/0c344259dcdc10a35840151bfe3ae1b27f9b53ff) | fix | update peer dependency to reflect TS 4.6 support | +| [044101554](https://github.com/angular/angular-cli/commit/044101554dfbca07d74f2a4391f94875df7928d2) | perf | use Webpack's built-in xxhash64 support | +| [9277eed1d](https://github.com/angular/angular-cli/commit/9277eed1d9603d5e258eb7ae27de527eba919482) | refactor | remove deprecated ivy namespace | + +## Special Thanks + +Adrien Crivelli, Alan Agius, Charles Lyding, Cédric Exbrayat, Daniil Dubrava, Doug Parker, Elton Coelho, George Kalpakas, Jason Bedard, Joey Perrott, Kristiyan Kostadinov, Paul Gschwendtner, Pawel Kozlowski, Tobias Speicher and alkavats1 + + + + + +# 13.3.7 (2022-05-25) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- | +| [a54018d8f](https://github.com/angular/angular-cli/commit/a54018d8f5f976034bf0a33f826245b7a6b74bbe) | fix | add debugging and timing information in JavaScript and CSS optimization plugins | + +## Special Thanks + +Alan Agius and Joey Perrott + + + + + +# 13.3.6 (2022-05-18) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------- | +| [e20964c43](https://github.com/angular/angular-cli/commit/e20964c43c52125b6d2bfa9bbea444fb2eea1e15) | fix | resolve relative schematic from `angular.json` instead of current working directory | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------ | +| [16fec8d58](https://github.com/angular/angular-cli/commit/16fec8d58b6ec421df5e7809c45838baf232b4a9) | fix | update `babel-loader` to 8.2.5 | + +## Special Thanks + +Alan Agius, Charles Lyding, Jason Bedard and Paul Gschwendtner + + + + + +# 13.3.5 (2022-05-04) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------- | +| [6da0910d3](https://github.com/angular/angular-cli/commit/6da0910d345eb84084e32a462432a508d518f402) | fix | update `@ampproject/remapping` to `2.2.0` | + +## Special Thanks + +Alan Agius, Charles Lyding and Paul Gschwendtner + + + + + +# 13.3.4 (2022-04-27) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------- | +| [f4da75656](https://github.com/angular/angular-cli/commit/f4da756560358273098df2a5cae7848201206c77) | fix | change wrapping of schematic code | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [5d0141bfb](https://github.com/angular/angular-cli/commit/5d0141bfb4ae80b1a7543eab64e9c381c932eaef) | fix | correctly resolve custom service worker configuration file | + +## Special Thanks + +Charles Lyding and Wagner Maciel + + + + + +# 13.3.3 (2022-04-13) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [d38b247cf](https://github.com/angular/angular-cli/commit/d38b247cf19edf5ecf7792343fa2bc8c05a3a8b8) | fix | display debug logs when using the `--verbose` option | + +### @angular-devkit/build-webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------- | +| [5682baee4](https://github.com/angular/angular-cli/commit/5682baee4b562b314dad781403dcc0c46e0a8abb) | fix | emit devserver setup errors | + +## Special Thanks + +Alan Agius + + + + + +# 13.3.2 (2022-04-06) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [49dc63d09](https://github.com/angular/angular-cli/commit/49dc63d09a7a7f2b7759b47e79fac934b867e9b4) | fix | ensure lint command auto-add exits after completion | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------- | +| [bbe74b87e](https://github.com/angular/angular-cli/commit/bbe74b87e52579c06b911db6173f33c67b8010a6) | fix | provide actionable error message when routing declaration cannot be found | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------- | +| [c97c8e7c9](https://github.com/angular/angular-cli/commit/c97c8e7c9bbcad66ba80967681cac46042c3aca7) | fix | update `minimatch` dependency to `3.0.5` | + +## Special Thanks + +Alan Agius, Charles Lyding and Morga Cezary + + + + + +# 13.3.1 (2022-03-30) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------- | +| [cf3cb2ecf](https://github.com/angular/angular-cli/commit/cf3cb2ecf9ca47a984c4272f0094f2a1c68c7dfe) | fix | fix extra comma added when use --change-detection=onPush and --style=none to generate a component | + +### @angular-devkit/architect-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [9f8d4dea0](https://github.com/angular/angular-cli/commit/9f8d4dea0449e236de7b928c5cc97e597a6f5844) | fix | update `minimist` to `1.2.6` | + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [ba3486de9](https://github.com/angular/angular-cli/commit/ba3486de94e733addf0ac17706b806dd813c9046) | fix | update `minimist` to `1.2.6` | + +### @angular-devkit/benchmark + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [1f7fa6970](https://github.com/angular/angular-cli/commit/1f7fa6970e8cddb2ba0c42df0e048a57292b7fe8) | fix | update `minimist` to `1.2.6` | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [293526c31](https://github.com/angular/angular-cli/commit/293526c31db9f0becc0ffc2d60999c80afa8a308) | fix | add `node_modules` prefix to excludes RegExp | +| [58ed97410](https://github.com/angular/angular-cli/commit/58ed97410b760909d523b05c3b4a06364e3c9a0f) | fix | allow Workers in Stackblitz | +| [4cd2331d3](https://github.com/angular/angular-cli/commit/4cd2331d34e2a9ab2ed78edf0284dbfefef511a5) | fix | don't override asset info when updating assets | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [c7c75820f](https://github.com/angular/angular-cli/commit/c7c75820f1d4ef827336626b78c8c3e5c0bd1f00) | fix | add Angular CLI major version as analytics dimension | + +## Special Thanks + +Alan Agius and gauravsoni119 + + + + + +# 12.2.17 (2022-03-31) + +### @angular-devkit/architect-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [ccb0f95f3](https://github.com/angular/angular-cli/commit/ccb0f95f33ff0d23a0ff9b237d0d78fc4c864787) | fix | update `minimist` to `1.2.6` | + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [abcdf4df2](https://github.com/angular/angular-cli/commit/abcdf4df20c29907ee28a38842942464addcf259) | fix | update `minimist` to `1.2.6` | + +### @angular-devkit/benchmark + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [2656a330e](https://github.com/angular/angular-cli/commit/2656a330eb365f37c3b6f8894436b4449d157e63) | fix | update `minimist` to `1.2.6` | + +## Special Thanks + +Alan Agius + + + + + +# 11.2.19 (2022-03-30) + +### @angular-devkit/architect-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [75caa1143](https://github.com/angular/angular-cli/commit/75caa1143f4007c9550ab0dabb62ae4df91e3827) | fix | update `minimist` to `1.2.6` | + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [80d479e9f](https://github.com/angular/angular-cli/commit/80d479e9fdfcf6863ebbe0986ea6cd29309f398d) | fix | update `minimist` to `1.2.6` | + +### @angular-devkit/benchmark + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [f61cd1a79](https://github.com/angular/angular-cli/commit/f61cd1a79b6960711d4aa5b16d04308bbdc67beb) | fix | update `minimist` to `1.2.6` | + +## Special Thanks + +Alan Agius and Doug Parker + + + + + +# 13.3.0 (2022-03-16) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------- | +| [c995ed5e8](https://github.com/angular/angular-cli/commit/c995ed5e8a8e1b20cf376f4c48c5141fd5f4548a) | feat | support TypeScript 4.6 | + +## Special Thanks + +Alan Agius and Doug Parker + + + + + +# 13.2.6 (2022-03-09) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------ | +| [90a5531b1](https://github.com/angular/angular-cli/commit/90a5531b1fbe4043ab47f921ad6b858d34e7c7d0) | fix | ignore css only chunks during naming | + +## Special Thanks + +Alan Agius, Charles Lyding and Daniele Maltese + + + + + +# 13.2.5 (2022-02-23) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- | +| [acf1e5e4a](https://github.com/angular/angular-cli/commit/acf1e5e4a5b359be125272f7e4055208116a13d8) | fix | don't rename blocks which have a name | +| [7a493979c](https://github.com/angular/angular-cli/commit/7a493979ccb71e974d668fca67d75e1b194f8608) | fix | update `terser` to `5.11.0` | + +## Special Thanks + +Alan Agius and Paul Gschwendtner + + + + + +# 13.2.4 (2022-02-17) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | +| [48c655ac9](https://github.com/angular/angular-cli/commit/48c655ac98e1d69622dd832c6a915c48e703cd8f) | fix | update `esbuild` to `0.14.22` | +| [c0736ea0b](https://github.com/angular/angular-cli/commit/c0736ea0b173861bb5ceb9315d27038eb28535e1) | fix | update license-webpack-plugin to 4.0.2 | + +## Special Thanks + +Alan Agius, Anner Visser and Charles Lyding + + + + + +# 13.2.3 (2022-02-09) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------- | +| [8c8377fee](https://github.com/angular/angular-cli/commit/8c8377fee4999266f4e58bf3c3091100d4393df7) | fix | block Karma from starting until build is complete | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [1317e470e](https://github.com/angular/angular-cli/commit/1317e470ec74d1dd9dced2d0ec0022abfe921995) | fix | support locating PNPM lock file during NGCC processing | + +## Special Thanks + +Alan Agius, Derek Cormier and Joey Perrott + + + + + +# 13.2.2 (2022-02-02) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [cc5505cfc](https://github.com/angular/angular-cli/commit/cc5505cfcf12732fad4f85e6e76c8e4f0584c13a) | fix | add `whatwg-url` to downlevel exclusion list | +| [ff54b49e7](https://github.com/angular/angular-cli/commit/ff54b49e7097cda2eb835bc8c9674f71fcc91c3c) | fix | ensure to use content hash as filenames hashing mechanism | +| [b0e2bb289](https://github.com/angular/angular-cli/commit/b0e2bb289050efc77478a0f50778abbec9c5a318) | perf | update `license-webpack-plugin` to `4.0.1` | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------- | +| [c8826a973](https://github.com/angular/angular-cli/commit/c8826a9738f860e374bd65a058c6be1b02545133) | fix | correctly resolve schema references defaults | + +## Special Thanks + +Alan Agius, Derek Cormier and Joey Perrott + + + + + +# 13.2.1 (2022-01-31) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------- | +| [acd752773](https://github.com/angular/angular-cli/commit/acd752773d85e4debbc2b415c7ea369bc3d7018a) | fix | invalid browsers version ranges | + +## Special Thanks + +Alan Agius + + + + + +# 13.2.0 (2022-01-26) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [41a828e20](https://github.com/angular/angular-cli/commit/41a828e2068b881f744846c3f0edbff8c62cb9ce) | fix | updated Angular new project version to v13.2.0-next.0 | + +### @angular-devkit/architect + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------- | +| [f2c6b2b7e](https://github.com/angular/angular-cli/commit/f2c6b2b7ec88a1b7e45884b38faa0978af1b4b74) | fix | correctly handle ESM builders | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------- | +| [cbe028e37](https://github.com/angular/angular-cli/commit/cbe028e37c8af6f2e17cbbeddc968c9410151bbb) | feat | expose i18nDuplicateTranslation option of browser and server builders | +| [509322b62](https://github.com/angular/angular-cli/commit/509322b6214b3425bd209087ac99ee9b14edeaba) | fix | Don't use TAILWIND_MODE=watch | + +### @angular-devkit/build-webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | +| [820ff2a3e](https://github.com/angular/angular-cli/commit/820ff2a3e84c5a55e23359e3a45714db83362a2a) | fix | correctly handle ESM webpack configurations | + +## Special Thanks + +Alan Agius, Cédric Exbrayat, Derek Cormier, Doug Parker, Joey Perrott, Jordan Pittman, grant-wilson and minijus + + + + + +# 13.1.4 (2022-01-19) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [2f2069dba](https://github.com/angular/angular-cli/commit/2f2069dbaa70c3d4725923f1c3ccbf56b1f57576) | fix | disable parsing `new URL` syntax | +| [bddd0fb9f](https://github.com/angular/angular-cli/commit/bddd0fb9f34a8706dd1646952eed08970b9cddbe) | fix | support ESNext as target for JavaScript optimizations | + +## Special Thanks + +Alan Agius, Derek Cormier and Doug Parker + + + + + +# 13.1.3 (2022-01-12) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------- | +| [4c9d72c65](https://github.com/angular/angular-cli/commit/4c9d72c659d912bd9ef4590a2e88340932a96868) | fix | remove extra space in `Unable to find compatible package` during `ng add` | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------- | +| [9b07191b1](https://github.com/angular/angular-cli/commit/9b07191b1ccdcd2a6bb17686471acddd5862dcf5) | fix | set `skipTest` flag for resolvers when using ng new --skip-tests | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [5b39e0eca](https://github.com/angular/angular-cli/commit/5b39e0eca6e8a3825f66ad6cd1818e551bf98f08) | fix | automatically purge stale build cache entries | +| [6046e06b9](https://github.com/angular/angular-cli/commit/6046e06b926af29f89c605504f5356ec553c6390) | fix | correctly resolve `core-js/proposals/reflect-metadata` | +| [de68daa55](https://github.com/angular/angular-cli/commit/de68daa5581dd1f257382da16704d442b540ec41) | fix | enable `:where` CSS pseudo-class | +| [6a617ff4a](https://github.com/angular/angular-cli/commit/6a617ff4a2fe75968965dc5dcf0f3ba7bae92935) | fix | ensure `$localize` calls are replaced in watch mode | +| [92b4e067b](https://github.com/angular/angular-cli/commit/92b4e067b24bdcd1bb7e40612b5355ce61e040ce) | fix | load translations fresh start | +| [d674dcd1a](https://github.com/angular/angular-cli/commit/d674dcd1af409910dd4f41ac676349aee363ebdb) | fix | localized bundle generation fails in watch mode | +| [6876ad36e](https://github.com/angular/angular-cli/commit/6876ad36efaadac5c4d371cff96c9a4cfa0e3d2b) | fix | use `contenthash` instead of `chunkhash` for chunks | +| [11fd02105](https://github.com/angular/angular-cli/commit/11fd02105908e155c4a9c7f87e9641127cc2f378) | fix | websocket client only injected if required | +| [6ca0e41a9](https://github.com/angular/angular-cli/commit/6ca0e41a9b54aef0a8ea626be73e06d19370f3a7) | perf | update `esbuild` to `0.14.11` | + +## Special Thanks + +Alan Agius, Bill Barry, Derek Cormier, Elio Goettelmann, Joey Perrott, Kasper Christensen, Lukas Spirig and Zoltan Lehoczky + + + + + +# 12.2.15 (2022-01-12) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [526115fdb](https://github.com/angular/angular-cli/commit/526115fdb7d35ff01f5dbdb6027d9f5e925e4056) | fix | updated webpack-dev-server to latest security patch | + +## Special Thanks + +Doug Parker and iRealNirmal + + + +# 11.2.18 (2022-01-12) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [534678450](https://github.com/angular/angular-cli/commit/534678450196a45610e88a85ee01317aa43dc788) | fix | updated webpack-dev-server to latest security patch | + +## Special Thanks + +Doug Parker and iRealNirmal + + + +# 13.2.0-next.1 (2021-12-15) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [41a828e20](https://github.com/angular/angular-cli/commit/41a828e2068b881f744846c3f0edbff8c62cb9ce) | fix | updated Angular new project version to v13.2.0-next.0 | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [0323a35b4](https://github.com/angular/angular-cli/commit/0323a35b47a4a2fd3870b09d46e3655714e50abd) | fix | add `tailwindcss` support for version 3 | +| [471930007](https://github.com/angular/angular-cli/commit/471930007cb9cd26264eab483fdfd1f5b4db6641) | fix | display FS cache information when `verbose` option is used | +| [f1d2873ca](https://github.com/angular/angular-cli/commit/f1d2873ca7ee337748366d04878514c2c27a72a2) | fix | only extract CSS styles when are specified in `styles` option | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [b03b9eefe](https://github.com/angular/angular-cli/commit/b03b9eefeac77b93931803de208118e3a6c5a928) | perf | reduce redundant module rebuilds when cache is restored | + +## Special Thanks + +Alan Agius, Cédric Exbrayat, Derek Cormier and Doug Parker + + + + + +# 13.1.2 (2021-12-15) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [1ddbd75ae](https://github.com/angular/angular-cli/commit/1ddbd75ae200c14b5f33556bd6d5ae6b7722d14e) | fix | add `tailwindcss` support for version 3 | +| [adf925c07](https://github.com/angular/angular-cli/commit/adf925c0755b6e78a57932becdb7b7a764afb9e6) | fix | display FS cache information when `verbose` option is used | +| [09c3826c9](https://github.com/angular/angular-cli/commit/09c3826c9d9128a6b520d0fe8da3cb466d18cddc) | fix | only extract CSS styles when are specified in `styles` option | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [f31d7f79d](https://github.com/angular/angular-cli/commit/f31d7f79dfa8f997fecdcfec1ebc6cfbe657f3fb) | perf | reduce redundant module rebuilds when cache is restored | + +## Special Thanks + +Alan Agius, Derek Cormier and Doug Parker + + + + + +# 11.2.17 (2021-12-16) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [1efff8f82](https://github.com/angular/angular-cli/commit/1efff8f82df38b7485f8a8dcdd5bfea5a457c6a1) | fix | exclude deprecated packages with removal migration from update | + +## Special Thanks + +Alan Agius and Doug Parker + + + + + +# 11.2.16 (2021-12-15) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------------- | +| [f456b0962](https://github.com/angular/angular-cli/commit/f456b0962b9f339759bc86c092256f68d68d9ecf) | fix | error when updating Angular packages across multi-major migrations | +| [886d2511e](https://github.com/angular/angular-cli/commit/886d2511e292b687acce1ac4c6924f992494d14f) | fix | logic which determines which temp version of the CLI is to be download during `ng update` | +| [776d1210a](https://github.com/angular/angular-cli/commit/776d1210a9e62bf2531d977138f49f93820a8b87) | fix | update `ng update` output for Angular packages | + +## Special Thanks + +Alan Agius and Doug Parker + + + + + +# 10.2.4 (2021-12-15) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------------- | +| [745d77728](https://github.com/angular/angular-cli/commit/745d777288a5ae0e79b4ecdf7b8483f242ba8e66) | fix | error when updating Angular packages across multi-major migrations | +| [460ea21b5](https://github.com/angular/angular-cli/commit/460ea21b5d4b8759a3f7457b885110022dd21dfc) | fix | logic which determines which temp version of the CLI is to be download during `ng update` | +| [03da12899](https://github.com/angular/angular-cli/commit/03da1289996790ae574a49bb46123c74417a97c2) | fix | update `ng update` output for Angular packages | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------ | +| [d6582d489](https://github.com/angular/angular-cli/commit/d6582d48944f7bf169f3902e4c19186a6751f473) | fix | change `karma-jasmine-html-reporter` dependency to use tilde | + +## Special Thanks + +Alan Agius, Charles Lyding, Doug Parker and Joey Perrott + + + + + +# 13.1.1 (2021-12-10) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [a315b968a](https://github.com/angular/angular-cli/commit/a315b968a36e6aae990e52d9a18673fef9b5fda6) | fix | updated Angular new project version to v13.1.0 | + +## Special Thanks + +Alan Agius, Cédric Exbrayat and Derek Cormier + + + + + +# 13.1.0 (2021-12-09) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------ | +| [56f802b7d](https://github.com/angular/angular-cli/commit/56f802b7dd26bfc774b6b00982a1dbbe0bafddd0) | feat | ask to install angular-eslint when running ng lint in new projects | +| [ecd9fb5c7](https://github.com/angular/angular-cli/commit/ecd9fb5c774b6301348c4514da04d58ae8903d06) | feat | provide more detailed error for not found builder | +| [0b6071af3](https://github.com/angular/angular-cli/commit/0b6071af3a51e7d3f38a661bd4e0a3c3e81aff2f) | fix | `ng doc` does open browser on Windows | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------- | +| [d5d9f042f](https://github.com/angular/angular-cli/commit/d5d9f042f2ea42573b7ff4fab90cab85d0c5ec0b) | feat | add VS Code configurations when generating a new workspace | +| [f95cc8281](https://github.com/angular/angular-cli/commit/f95cc8281a64bd9ac19e0fa5d92cb0a6ee8c32ec) | feat | generate new projects using TypeScript 4.5 | +| [21809e14c](https://github.com/angular/angular-cli/commit/21809e14cd5c666c82fdaebc9e601341dfb76d0a) | feat | loosen project name validation | + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------ | +| [339bab06c](https://github.com/angular/angular-cli/commit/339bab06cc25863571acb09cb3e877fed14ca2f9) | feat | generate new projects using TypeScript 4.5 | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- | +| [bc8563760](https://github.com/angular/angular-cli/commit/bc856376039287cf5fb6135ca5da65a9000f5664) | feat | add estimated transfer size to build output report | +| [bc17cf0cd](https://github.com/angular/angular-cli/commit/bc17cf0cdd02bf50758e510756a26e6e6ca32d14) | feat | colorize file raw sizes based on failing budgets | +| [3c681b68d](https://github.com/angular/angular-cli/commit/3c681b68d7a32f1cfaf3feee6b2e02cc6e0f0568) | feat | set `dir` attribute when using localization | +| [6d0f99a2d](https://github.com/angular/angular-cli/commit/6d0f99a2deef957c15836c172b9f68f716f836a4) | feat | support JSON comments in dev-server proxy configuration file | +| [9300545e6](https://github.com/angular/angular-cli/commit/9300545e6148b4548cc02bb6a311a2f0e2bb79c5) | feat | watch i18n translation files with dev server | +| [9bacba342](https://github.com/angular/angular-cli/commit/9bacba3420cda7897091522415a8d55cf1b75106) | fix | differentiate components and global styles using file query instead of filename | +| [7408511da](https://github.com/angular/angular-cli/commit/7408511da555f37560ca7e3b536e15dfc8f6a1e5) | fix | display cleaner errors | +| [d55fc62ef](https://github.com/angular/angular-cli/commit/d55fc62ef2f8bc7a6f1190f56f8e8b64c9195263) | fix | fallback to use language ID to set the `dir` attribute | +| [4c288b8bd](https://github.com/angular/angular-cli/commit/4c288b8bd28e7215887aa52025c4fa41fcf7bc01) | fix | lazy modules bundle budgets | +| [562dc6a89](https://github.com/angular/angular-cli/commit/562dc6a8924826509d9012b2c0fe61c089077399) | fix | prefer ES2015 entrypoints when application targets ES2019 or lower | +| [ac66e400c](https://github.com/angular/angular-cli/commit/ac66e400cddc81bde46949d1abe4560185dfbedb) | fix | Sass compilation in StackBlitz webcontainers | +| [e1bac5bbb](https://github.com/angular/angular-cli/commit/e1bac5bbb36f391b89445ba61abe561c75746f30) | fix | update Angular peer dependencies to v13.1 prerelease | +| [789ddfaeb](https://github.com/angular/angular-cli/commit/789ddfaeb0fcbc9aab1581384b88c3618e606c4b) | perf | disable webpack backwards compatible APIs | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [5402f99f8](https://github.com/angular/angular-cli/commit/5402f99f8ad20e0a57456a416a992415fc6332bd) | fix | add `cjs` and `mjs` to passthrough files | +| [10d4ede2d](https://github.com/angular/angular-cli/commit/10d4ede2de42dfc302dcb4c5790274290170568d) | fix | handle promise rejection during Angular program analyzes | + +## Special Thanks + +Alan Agius, Charles Lyding, Doug Parker, Ferdinand Malcher, Joey Perrott and Ruslan Lekhman + + + + + +# 12.2.14 (2021-12-07) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------------- | +| [30295b33e](https://github.com/angular/angular-cli/commit/30295b33ed74667f31e9d3a4a0017910a85fd734) | fix | error when updating Angular packages across multi-major migrations | +| [e07bd059e](https://github.com/angular/angular-cli/commit/e07bd059e3d6bc6b40191c036c467595ed119da7) | fix | logic which determines which temp version of the CLI is to be download during `ng update` | +| [ce1ec0420](https://github.com/angular/angular-cli/commit/ce1ec0420770a8e28c1c1301df9e5eb4548d4c53) | fix | update `ng update` output for Angular packages | +| [dd9f8df52](https://github.com/angular/angular-cli/commit/dd9f8df5204d639272f183795ebd48d7994df427) | fix | update `pacote` to `12.0.2` | + +## Special Thanks + +Alan Agius and Doug Parker + + + + + +# 13.0.4 (2021-12-01) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------------- | +| [ded7b5c06](https://github.com/angular/angular-cli/commit/ded7b5c069a145d1b3e264538d7c4302919ad030) | fix | exit with a non-zero error code when migration fails during `ng update` | +| [250a58b48](https://github.com/angular/angular-cli/commit/250a58b4820a738aba7609627fa7fce0a24f10db) | fix | logic which determines which temp version of the CLI is to be download during `ng update` | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------- | +| [372e2e633](https://github.com/angular/angular-cli/commit/372e2e633f4bd9bf29c35d02890e1c6a70da3169) | fix | address eslint linting failures in `test.ts` | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------- | +| [b835389c8](https://github.com/angular/angular-cli/commit/b835389c8a60749151039ed0baf0be025ce0932b) | fix | correctly extract messages when using cached build ([#22266](https://github.com/angular/angular-cli/pull/22266)) | +| [647a5f0b1](https://github.com/angular/angular-cli/commit/647a5f0b18e49b2ece3f43c0a06bfb75d7caef49) | fix | don't watch nested `node_modules` when polling is enabled | +| [4d01d4f72](https://github.com/angular/angular-cli/commit/4d01d4f72344c42f650f5495b21e6bd94069969a) | fix | transform remapped sourcemap into a plain object | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------- | +| [4d918ef99](https://github.com/angular/angular-cli/commit/4d918ef9912d53a09d73fb19fa41b121dceed37c) | fix | JIT mode CommonJS accessing inexistent `default` property | + +## Special Thanks + +Alan Agius, Billy Lando, David-Emmanuel DIVERNOIS and Derek Cormier + + + + + +# 13.0.3 (2021-11-17) + +## Special Thanks + +Alan Agius, Joey Perrott and Krzysztof Platis + + + + + +# 13.0.2 (2021-11-10) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------ | +| [34047b1ad](https://github.com/angular/angular-cli/commit/34047b1adccd7eb852c1900c872e9ca71c8d4cd9) | fix | avoid redirecting @angular/core in Angular migrations | +| [ff4538e98](https://github.com/angular/angular-cli/commit/ff4538e981cfff49b6e8433ffcb5ac2d2ea5d07e) | fix | favor ng-update `packageGroupName` in ng update output | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [1bc00b6fe](https://github.com/angular/angular-cli/commit/1bc00b6feb9033fd611dec965c82f03e4135a9f4) | fix | migrate ng-packagr configurations in package.json | +| [9ea74a13d](https://github.com/angular/angular-cli/commit/9ea74a13d07208373490c7cdb3ff7c452c698322) | fix | show warning when migrating ng-packagr JS configurations | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------- | +| [35164bf92](https://github.com/angular/angular-cli/commit/35164bf92b986a67215580622aaddc4148a7c822) | fix | don't restore `input` of type `file` during HMR | +| [facb5d8ff](https://github.com/angular/angular-cli/commit/facb5d8ffd4f6a81d3132515b8bae64278cf8316) | fix | don't show `[NG HMR] Unknown input type` when restoring file type input | +| [ef8815d04](https://github.com/angular/angular-cli/commit/ef8815d0434836f2d8119e91a7bc09742ff77d37) | fix | improve sourcemap fidelity during code-coverage | +| [966a1334a](https://github.com/angular/angular-cli/commit/966a1334a6502f5d4a18710ae22e739e62770101) | fix | suppress "@charset" must be the first rule in the file warning | +| [1cdc24da0](https://github.com/angular/angular-cli/commit/1cdc24da0105fad75221e3c145de12dafc601059) | fix | update Angular peer dependencies to 13.0 stable | + +## Special Thanks + +Alan Agius, Charles Lyding, Joey Perrott and Paul Gschwendtner + + + + + +# 13.0.1 (2021-11-03) + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------- | +| [40f599241](https://github.com/angular/angular-cli/commit/40f599241e278478c694580c9dec4f5cc34db011) | fix | updated Angular new project version to v13.0.0 | + +## Special Thanks + +Charles Lyding and Joey Perrott + + + + + +# 12.2.13 (2021-11-03) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [a2bd940e4](https://github.com/angular/angular-cli/commit/a2bd940e4ab44db57b0fc69d5346d2862a19c879) | fix | add verbose logging for differential loading and i18n | + +## Special Thanks + +Charles Lyding and Doug Parker + + + + + +# 13.0.0 (2021-11-03) + +## Breaking Changes + +### @angular/cli + +- We drop support for Node.js versions prior to `12.20`. + +### @schematics/angular + +- `classlist.js` and `web-animations-js` are removed from application polyfills and uninstalled from the package. These were only needed for compatibility with Internet Explorer, which is no longer needed now that Angular only supports evergreen browsers. See: https://angular.dev/reference/versions#browser-support. + +Add the following to the polyfills file for an app to re-add these packages: + +```typescript +import 'classlist.js'; +import 'web-animations-js'; +``` + +And then run: + +```sh +npm install classlist.js web-animations-js --save +``` + +- We removed several deprecated `@schematics/angular` deprecated options. +- `lintFix` have been removed from all schematics. `ng lint --fix` should be used instead. +- `legacyBrowsers` have been removed from the `application` schematics since IE 11 is no longer supported. +- `configuration` has been removed from the `web-worker` as it was unused. +- `target` has been removed from the `service-worker` as it was unused. + +### @angular-devkit/build-angular + +- Support for `karma-coverage-instanbul-reporter` has been dropped in favor of the official karma coverage plugin `karma-coverage`. + +- Support for `node-sass` has been removed. `sass` will be used by default to compile SASS and SCSS files. + +- `NG_PERSISTENT_BUILD_CACHE` environment variable option no longer have effect. Configure `cli.cache` in the workspace configuration instead. + +```json +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "cache": { + "enabled": true, + "path": ".custom-cache-path", + "environment": "all" + } + } + ... +} +``` + +- Calling `BuilderContext.scheduleBuilder()` with a builder from `@angular-devkit/build-angular` now requires passing the `target` property in the 3rd argument, like in the following example: + + ```typescript + context.scheduleBuilder('@angular-devkit/build-angular:ng-packagr', options, { + target: context.target, + }); + ``` + +- The automatic inclusion of Angular-required ES2015 polyfills to support ES5 browsers has been removed. Previously when targeting ES5 within the application's TypeScript configuration or listing an ES5 requiring browser in the browserslist file, Angular-required polyfills were included in the built application. However, with Angular no longer supporting IE11, there are now no browsers officially supported by Angular that would require these polyfills. As a result, the automatic inclusion of these ES2015 polyfills has been removed. Any polyfills manually added to an application's code are not affected by this change. + +- With this change a number of deprecated dev-server builder options which proxied to the browser builder have been removed. These options should be configured in the browser builder instead. + +The removed options are: + +- `aot` +- `sourceMap` +- `deployUrl` +- `baseHref` +- `vendorChunk` +- `commonChunk` +- `optimization` +- `progress` + +- With this change we removed several deprecated builder options +- `extractCss` has been removed from the browser builder. CSS is now always extracted. +- `servePathDefaultWarning` and `hmrWarning` have been removed from the dev-server builder. These options had no effect. + +- Deprecated `@angular-devkit/build-angular:tslint` builder has been removed. Use https://github.com/angular-eslint/angular-eslint instead. + +- Differential loading support has been removed. With Angular no longer supporting IE11, there are now no browsers officially supported by Angular that require ES5 code. As a result, differential loading's functionality for creating and conditionally loading ES5 and ES2015+ variants of an application is no longer required. + +- TypeScript versions prior to 4.4 are no longer supported. + +- The dev-server now uses WebSockets to communicate changes to the browser during HMR and live-reloaded. If during your development you are using a proxy you will need to enable proxying of WebSockets. + +- We remove inlining of Google fonts in WOFF format since IE 11 is no longer supported. Other supported browsers use WOFF2. + +### @angular-devkit/build-webpack + +- Support for `webpack-dev-server` version 3 has been removed. For more information about the migration please see: https://github.com/webpack/webpack-dev-server/blob/master/migration-v4.md + +Note: this change only affects users depending on `@angular-devkit/build-webpack` directly. + +### @angular-devkit/core + +- With this change we drop support for the deprecated behaviour to transform `id` in schemas. Use `$id` instead. + +Note: this only effects schematics and builders authors. + +- The deprecated JSON parser has been removed from public API. [jsonc-parser](https://www.npmjs.com/package/jsonc-parser) should be used instead. + +### @angular-devkit/schematics + +- `isAction` has been removed without replacement as it was unused. + +- With this change we remove the following deprecated APIs +- `TslintFixTask` +- `TslintFixTaskOptions` + +**Note:** this only effects schematics developers. + +### @ngtools/webpack + +- Deprecated `inlineStyleMimeType` option has been removed from `AngularWebpackPluginOptions`. Use `inlineStyleFileExtension` instead. + +- Applications directly using the `webpack-cli` and not the Angular CLI to build must set the environment variable `DISABLE_V8_COMPILE_CACHE=1`. The `@ngtools/webpack` package now uses dynamic imports to provide support for the ESM `@angular/compiler-cli` package. The `v8-compile-cache` package used by the `webpack-cli` does not currently support dynamic import expressions and will cause builds to fail if the environment variable is not specified. Applications using the Angular CLI are not affected by this limitation. + +## Deprecations + +### + +- `@angular-devkit/build-optimizer` + +It's functionality has been included in `@angular-devkit/build-angular` so this package is no longer needed by the CLI and we will stop publishing the package soon. It has been an experimental (never hit `1.0.0`) and internal (only used by Angular itself) package and should be not be used directly by others. + +### @angular-devkit/build-angular + +- `NG_BUILD_CACHE` environment variable option will be removed in the next major version. Configure `cli.cache` in the workspace configuration instead. + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------- | +| [9fe55752d](https://github.com/angular/angular-cli/commit/9fe55752db8bb50cad5a1ddfe670dce06528e23e) | feat | officially support Node.js v16 | +| [5ad145722](https://github.com/angular/angular-cli/commit/5ad145722f66af526a36983b259c6d625c93f307) | fix | error when updating Angular packages across multi-major migrations | +| [e4bc35e33](https://github.com/angular/angular-cli/commit/e4bc35e332e378f8d238f4069dc56f422fe205d6) | fix | exclude packages from ng add that contain invalid peer dependencies | +| [e1b954d70](https://github.com/angular/angular-cli/commit/e1b954d707f90622d8a75fc45840cefeb224c286) | fix | keep relative migration paths during update analysis | +| [c3acf3cc2](https://github.com/angular/angular-cli/commit/c3acf3cc26b9e37a3b8f4c369f42731f46b522ee) | fix | remove unused cli project options. | +| [77fe6c4e6](https://github.com/angular/angular-cli/commit/77fe6c4e67147ff42fa6350edaf4ef7dc184a3a6) | fix | update `engines` to require `node` `12.20.0` | +| [8795536a3](https://github.com/angular/angular-cli/commit/8795536a31efbed6373787188cb21c5d1e0accbd) | fix | update `ng update` output for Angular packages | +| [d8c9f6eaf](https://github.com/angular/angular-cli/commit/d8c9f6eaf4513639741d20c6af97a751b33b968e) | fix | update the update command to fully support Node.js v16 | + +### @schematics/angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------- | +| [7ff8c5350](https://github.com/angular/angular-cli/commit/7ff8c5350ea2e49574dd659adae02215957d2685) | feat | add `/.angular/cache` to `.gitignore` | +| [3ba13f467](https://github.com/angular/angular-cli/commit/3ba13f467c12f4ad0c314cc92a2d94fb63f640ec) | feat | add `noImplicitOverride` and `noPropertyAccessFromIndexSignature` to workspace tsconfig | +| [268a03b63](https://github.com/angular/angular-cli/commit/268a03b63094d9c680401bc0977edafb22826ce3) | feat | add migration to update the workspace config | +| [7bdcd7da1](https://github.com/angular/angular-cli/commit/7bdcd7da1ff3a31f4958d90d856beb297e99b187) | feat | create new projects with rxjs 7 | +| [eac18aed7](https://github.com/angular/angular-cli/commit/eac18aed78da55efb840a3ef6f5e90718946504c) | feat | drop polyfills required only for Internet Explorer now that support has been dropped for it | +| [4f91816b2](https://github.com/angular/angular-cli/commit/4f91816b2951c0e2b0109ad1938eb0ae632c0c76) | feat | migrate libraries to be published from ViewEngine to Ivy Partial compilation | +| [5986befcd](https://github.com/angular/angular-cli/commit/5986befcdc953c0e8c90c756ac1c89b8c4b66614) | feat | remove deprecated options | +| [9fbd16655](https://github.com/angular/angular-cli/commit/9fbd16655e86ec6fc598a47436e3e80a48beb649) | feat | remove IE 11 specific polyfills | +| [a7b2e6f51](https://github.com/angular/angular-cli/commit/a7b2e6f512d2a1124f0d2c68caacfe6552a10cd5) | feat | update ngsw-config resources extensions | +| [732ef7985](https://github.com/angular/angular-cli/commit/732ef798523f74994ed3d482a65b191058674d19) | fix | add browserslist configuration in library projects | +| [585adacd0](https://github.com/angular/angular-cli/commit/585adacd0624ddf32c5c69a755d8e542f3463861) | fix | don't add `destroyAfterEach` in newly generated spec files | +| [e58226ee9](https://github.com/angular/angular-cli/commit/e58226ee948ea88f27a81d50d71945b5c9c39ee3) | fix | don't export `renderModuleFactory` from server file | +| [0ec0ad8a4](https://github.com/angular/angular-cli/commit/0ec0ad8a4dba4a778b368c5cd76ef13fb370b310) | fix | remove `target` and `lib` options for library tsconfig | +| [f227e145d](https://github.com/angular/angular-cli/commit/f227e145dfbec2954cb96c92ab3c4cb97cbe0f32) | fix | updated Angular new project version to v13.0 prerelease | + +### + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [5e435ff37](https://github.com/angular/angular-cli/commit/5e435ff37703f9ffea7fa92fbd5cd42d9a3db07e) | docs | mark `@angular-devkit/build-optimizer` as deprecated. | + +### @angular-devkit/architect + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------ | +| [09e039500](https://github.com/angular/angular-cli/commit/09e039500f34b0d6a16e62128409ac5821e8b9c2) | feat | include workspace extensions in project metadata | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------- | +| [f53bf9dc2](https://github.com/angular/angular-cli/commit/f53bf9dc21ee9aa8a682b8a82ee8a9870fa859e1) | feat | add `type=module` to all scripts tags | +| [e95ecb8ab](https://github.com/angular/angular-cli/commit/e95ecb8ab0382eb803741619c446d6cc7b215ba0) | feat | deprecate deployUrl | +| [7dcfffaff](https://github.com/angular/angular-cli/commit/7dcfffafff6f3d29bbe679a90cdf77b1292fec0b) | feat | drop support for `karma-coverage-instanbul-reporter` | +| [ac3fc2752](https://github.com/angular/angular-cli/commit/ac3fc2752f28761e1cd42157b59dcf2364ae5567) | feat | drop support for `node-sass` | +| [5904afd1d](https://github.com/angular/angular-cli/commit/5904afd1de3ffa0bb6cd1757795ba9abfce9e523) | feat | enable disk cache by default and provide configurable options | +| [22cd9edfa](https://github.com/angular/angular-cli/commit/22cd9edfafd357bb9d62a93dd56f033b3f34bbe8) | feat | favor es2020 main fields | +| [7576136b2](https://github.com/angular/angular-cli/commit/7576136b2fc8a9173b0a92e2ab14c9bc2559081e) | feat | remove automatic inclusion of ES5 browser polyfills | +| [000b0e51c](https://github.com/angular/angular-cli/commit/000b0e51c166ecd26b6f24d6a133ea5076df9849) | feat | remove deprecated dev-server options | +| [20e48a33c](https://github.com/angular/angular-cli/commit/20e48a33c14a1b0b959ba0a45018df53a3e129c8) | feat | remove deprecated options | +| [e78f6ab5d](https://github.com/angular/angular-cli/commit/e78f6ab5d8f00338d826c8407ce5c8fca40cf097) | feat | remove deprecated tslint builder | +| [701214d17](https://github.com/angular/angular-cli/commit/701214d174586fe7373b6155024c9b6e97b26377) | feat | remove differential loading support | +| [fb1ad7c5b](https://github.com/angular/angular-cli/commit/fb1ad7c5b3fa3df85f1d3dff3850e1ad0003ef9d) | feat | support ESM proxy configuration files for the dev server | +| [505438cc4](https://github.com/angular/angular-cli/commit/505438cc4146b1950038531ce30e1f62f7c41d00) | feat | support TypeScript 4.4 | +| [32dbf659a](https://github.com/angular/angular-cli/commit/32dbf659acb632fac1d76d99d8191ea9c5e6350b) | feat | update `webpack-dev-server` to version 4 | +| [c1efaa17f](https://github.com/angular/angular-cli/commit/c1efaa17feb1d2911dcdea12688d75086d410bf1) | fix | calculate valid Angular versions from peerDependencies | +| [d7af4a7b5](https://github.com/angular/angular-cli/commit/d7af4a7b536a7c43704f808ea208bc9f230d2403) | fix | enable custom `es2020` and `es2015` conditional exports | +| [f383f3201](https://github.com/angular/angular-cli/commit/f383f3201b69d28f8755c0bd63134619f9da408d) | fix | ESM-interop loaded plugin creators of `@angular/localize/tools` not respected | +| [7934becb5](https://github.com/angular/angular-cli/commit/7934becb581d07c8e1f74898ddd4c20f050be659) | fix | generate unique webpack runtimes | +| [b14e0a547](https://github.com/angular/angular-cli/commit/b14e0a54727352a6939c7a0ff13dffe2deaa67d2) | fix | improve sourcemaps fidelity when code coverage is enabled | +| [e19287453](https://github.com/angular/angular-cli/commit/e19287453c10740ea21b31a6c8a3cd5f3714955d) | fix | move `@angular/localize` detection prior to webpack initialization | +| [76d6d8826](https://github.com/angular/angular-cli/commit/76d6d8826f9968f84edf219f67b84673d70bbe95) | fix | set browserslist defaults | +| [167eed465](https://github.com/angular/angular-cli/commit/167eed4654be4480c45d7fdfe7a0b9f160170289) | fix | update Angular peer dependencies to v13.0 prerelease | +| [1d8cdf853](https://github.com/angular/angular-cli/commit/1d8cdf853dc8fdea78b067a715b3342ed9427caa) | fix | update esbuild to 0.13.12 | +| [884111ac0](https://github.com/angular/angular-cli/commit/884111ac0b8a73dca06d844b2ed795a3e3ed3289) | fix | update IE unsupported and deprecation messages | +| [4be6537dd](https://github.com/angular/angular-cli/commit/4be6537ddf4b32e8d204dbaa75f1a53712fe9d44) | fix | update TS/JS regexp checks to latest extensions | +| [427a9ee97](https://github.com/angular/angular-cli/commit/427a9ee9738c0911caeaba5fb4b59d183ffe6244) | fix | update workspace tsconfig lib es2020 | +| [ea926db25](https://github.com/angular/angular-cli/commit/ea926db257ad3b042af86178e472b5763a695146) | fix | use es2015 when generating server bundles | +| [13cceab8e](https://github.com/angular/angular-cli/commit/13cceab8e737a12d0809f184f852ceb5620d81fb) | fix | use URLs for absolute import paths with ESM | +| [4e0743c8a](https://github.com/angular/angular-cli/commit/4e0743c8ad5879f212f2ea232ac9492848a8df2c) | perf | change webpack hashing function to `xxhash64` | +| [cb7d156c2](https://github.com/angular/angular-cli/commit/cb7d156c23a7ef2f1c2f338db1487b85f8b98690) | perf | use esbuild as a CSS optimizer for global styles | +| [8e82263c5](https://github.com/angular/angular-cli/commit/8e82263c5e7da6ca25bdd4e2ce9ad2c775d623b7) | perf | use esbuild/terser combination to optimize global scripts | +| [e82eef924](https://github.com/angular/angular-cli/commit/e82eef924eb172a98fa157a958bde2cfcaa52ce6) | refactor | remove WOFF handling from inline-fonts processor | + +### @angular-devkit/build-webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------- | +| [a0b5897d5](https://github.com/angular/angular-cli/commit/a0b5897d50a00ee4668029c2cbc47cacd2ab925f) | feat | update `webpack-dev-server` to version 4 | +| [9efcb32e3](https://github.com/angular/angular-cli/commit/9efcb32e378442714eae4caec43281123c5e30f6) | fix | better handle concurrent dev-servers | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------- | +| [0c92ea5ca](https://github.com/angular/angular-cli/commit/0c92ea5ca34d82849862d55c4210cf62c819d514) | feat | remove deprecated schema id handling | +| [9874aff71](https://github.com/angular/angular-cli/commit/9874aff71ecb5f3baf6c1dcc489581d1dcb58491) | fix | add missing option peer dependency on `chokidar` | +| [a54e5e065](https://github.com/angular/angular-cli/commit/a54e5e06551c828eb5cf08695674e04fd8a78bf3) | fix | support Node.js v16 with `NodeJsSyncHost`/`NodeJsAsyncHost` delete operation | +| [d722fdf1f](https://github.com/angular/angular-cli/commit/d722fdf1f67c394762906794605bc1ad657670d1) | refactor | remove deprecated JSON parser | + +### @angular-devkit/schematics + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [0565ed62e](https://github.com/angular/angular-cli/commit/0565ed62eb08c1e82cffb2533e6afde216c37eb7) | feat | add UpdateBuffer2 based on magic-string | +| [8954d1152](https://github.com/angular/angular-cli/commit/8954d1152b6c1a33dd7d4b63d2fa430d91e7b370) | feat | remove deprecated `isAction` | +| [053b7d66c](https://github.com/angular/angular-cli/commit/053b7d66c269423804891e4d43d61f8605838e24) | feat | remove deprecated tslint APIs | +| [bdd89ae84](https://github.com/angular/angular-cli/commit/bdd89ae84ad6919b670dde862de72f562c86d0c5) | fix | handle zero or negative length removals in update buffer | + +### @ngtools/webpack + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------- | +| [d2a97f919](https://github.com/angular/angular-cli/commit/d2a97f9193fcf7e454fe8eb48c0ed732d3b2f24f) | fix | update Angular peer dependencies to v13.0 prerelease | +| [7928b18ed](https://github.com/angular/angular-cli/commit/7928b18edf34243a404b5a4f40a5d6e40247d797) | perf | reduce repeat path mapping analysis during resolution | +| [8ce8e4edc](https://github.com/angular/angular-cli/commit/8ce8e4edc5ca2984d6a36fe4c7d308fa7f089102) | refactor | remove deprecated `inlineStyleMimeType` option | +| [7d98ab3df](https://github.com/angular/angular-cli/commit/7d98ab3df9f7c15612c69cedca5a01a535301508) | refactor | support an ESM-only `@angular/compiler-cli` package | + +## Special Thanks + +Alan Agius, Charles Lyding, Doug Parker, Douglas Parker, Joey Perrott, Kristiyan Kostadinov, Lukas Spirig and Paul Gschwendtner + + + + + +# 12.2.9 (2021-10-06) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------- | +| [9d45b7752](https://github.com/angular/angular-cli/commit/9d45b77522a9693c4876fdfd741e8869e89e0268) | fix | add web-streams-polyfill to downlevel exclusion list | +| [ccedf53a8](https://github.com/angular/angular-cli/commit/ccedf53a820a748b56c84528294b36c7af30dbaf) | fix | update `esbuild` to `0.13.4` | + +## Special Thanks + +Alan Agius and Charles Lyding + + + + + +# 12.2.8 (2021-10-01) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------- | +| [821a1b5a9](https://github.com/angular/angular-cli/commit/821a1b5a949d53f2e82f734062b711a166d42e24) | fix | babel adjust enum plugin incorrectly transforming loose enums | + +## Special Thanks + +Paul Gschwendtner + + + + + +# 12.2.7 (2021-09-22) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------- | +| [d856b4d23](https://github.com/angular/angular-cli/commit/d856b4d2369bea76ce65fc5f6d1585145ad41618) | fix | support WASM-based esbuild optimizer fallback | + +## Special Thanks + +Alan Agius and Charles Lyding + + + + + +# 12.2.6 (2021-09-15) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [8b21effad](https://github.com/angular/angular-cli/commit/8b21effad673877cf1a82ef7d0601393a65517fb) | fix | handle `FORCE_COLOR` when stdout is not instance of `WriteStream` | + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------- | +| [ea60f0f52](https://github.com/angular/angular-cli/commit/ea60f0f527f2ab8fc5acc967138c4ae993946923) | fix | handle `FORCE_COLOR` when stdout is not instance of `WriteStream` | + +## Special Thanks + +Alan Agius + + + + + +# 12.2.5 (2021-09-08) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [0498768c5](https://github.com/angular/angular-cli/commit/0498768c54de225a40c28fdf27bb1fc43959ba20) | fix | disable dev-server response compression | +| [367fce2e9](https://github.com/angular/angular-cli/commit/367fce2e9f9389c41f2ed5361ef6749198c49785) | fix | improve Safari browserslist to esbuild target conversion | + +## Special Thanks: + +Alan Agius and Charles Lyding + + + + + +# 12.2.4 (2021-09-01) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------- | +| [aaadef026](https://github.com/angular/angular-cli/commit/aaadef02698ba729ca04ccd4159bda5b6582babb) | fix | update `esbuild` to `0.12.24` | +| [f8a9f4a01](https://github.com/angular/angular-cli/commit/f8a9f4a0100286b7cf656ffbe486c3424cad5172) | fix | update `mini-css-extract-plugin` to `2.2.1` | + +## Special Thanks + +Alan Agius + + + + + +# 12.2.3 (2021-08-26) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------- | +| [3e3321857](https://github.com/angular/angular-cli/commit/3e33218578007f93a131dc8be569e9985179098f) | fix | RGBA converted to hex notation in component styles breaks IE11 | + +## Special Thanks: + +Alan Agius and Trevor Karjanis + + + + + +# 12.2.2 (2021-08-18) + +### @angular-devkit/build-angular + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| [a55118a75](https://github.com/angular/angular-cli/commit/a55118a753555c0082cfd434379559df7e3eb7f9) | fix: provide supported browsers to esbuild | +| [81baa4f95](https://github.com/angular/angular-cli/commit/81baa4f956443fcc718f9021fd23ab7064d04607) | fix: update Angular peer dependencies to 12.2 stable | +| [297410ae8](https://github.com/angular/angular-cli/commit/297410ae860860d71905639cf38b49ff05813845) | fix: handle undefined entrypoints when marking async chunks | + +### @ngtools/webpack + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| [b7199f366](https://github.com/angular/angular-cli/commit/b7199f366841d976b502ad5f1923e24ea2f6b302) | fix: update Angular peer dependencies to 12.2 stable | + +## Special Thanks: + +Alan Agius, Charles Lyding, Joey Perrott and Simon Primetzhofer + + + + + +# 12.2.1 (2021-08-11) + +### @angular/cli + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| [8dc3c895a](https://github.com/angular/angular-cli/commit/8dc3c895a6531316e672031c8d0815781f0c089a) | fix(@angular/cli): show error when using non-TTY terminal without passing `--skip-confirmation` during `ng add` | + +### @angular-devkit/schematics-cli + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| [eded01270](https://github.com/angular/angular-cli/commit/eded01270f9aa70f6ba4806a068de8d1c0a52454) | fix(@angular-devkit/schematics-cli): log when in debug and/or dry run modes | + +### @angular-devkit/build-angular + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [22e0208a9](https://github.com/angular/angular-cli/commit/22e0208a9ee6257213b3bf93ac61a2c3d4ac9504) | fix(@angular-devkit/build-angular): ensure native async is downlevelled in third-party libraries | +| [9b4b86fb0](https://github.com/angular/angular-cli/commit/9b4b86fb0d9c88a3c714f5eabf925859bb7b71bb) | fix(@angular-devkit/build-angular): support both pure annotation forms for static properties | +| [cea028090](https://github.com/angular/angular-cli/commit/cea0280908db39308ac5fa37374b138ceb79ecea) | fix(@angular-devkit/build-angular): do not consume inline sourcemaps when vendor sourcemaps is disabled. | +| [e7ec0346e](https://github.com/angular/angular-cli/commit/e7ec0346e69c090ded7d9ec6d3574deb79926db0) | fix(@angular-devkit/build-angular): avoid attempting to optimize copied JavaScript assets | +| [4f757c2bc](https://github.com/angular/angular-cli/commit/4f757c2bcf1356d33eaa86bc3b715c0a6b7c2ed8) | fix(@angular-devkit/build-angular): handle null maps in JavaScript optimizer worker | + +## Special Thanks: + +Alan Agius and Charles Lyding + + + + + +# 12.2.0 (2021-08-04) + +### @angular/cli + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| [259e26979](https://github.com/angular/angular-cli/commit/259e26979ebc712ee08fd36fb68a9576c1e02447) | fix(@angular/cli): merge npmrc files values | +| [c1eddbdc9](https://github.com/angular/angular-cli/commit/c1eddbdc98631fdfff287ce566d79ed43b601e0f) | fix(@angular/cli): handle `YARN_` environment variables during `ng update` and `ng add` | +| [6b00d1270](https://github.com/angular/angular-cli/commit/6b00d1270acaf33f32ee68c4254ce06951ddcb8c) | fix(@angular/cli): handle NPM_CONFIG environment variables during ng update and ng add | +| [88ee85c41](https://github.com/angular/angular-cli/commit/88ee85c4178e37b72001e8946b70a46ba739a0b7) | fix(@angular/cli): disable update notifier when retrieving package manager version during `ng version` | + +### @angular-devkit/build-angular + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| [d750c686f](https://github.com/angular/angular-cli/commit/d750c686fd26f3ccfccb039027bd816a91279497) | fix(@angular-devkit/build-angular): add priority to copy-webpack-plugin patterns | +| [4bcd1dc9e](https://github.com/angular/angular-cli/commit/4bcd1dc9ee744343a465d73d51d4a062964a3714) | fix(@angular-devkit/build-angular): allow classes with pure annotated static properties to be optimized | +| [ceade0c27](https://github.com/angular/angular-cli/commit/ceade0c27e4b8b0e731e6ca5128fd86cf071d029) | fix(@angular-devkit/build-angular): dasherize disable-host-check suggestion | +| [8383c6b42](https://github.com/angular/angular-cli/commit/8383c6b421f7005a25a3bff0826048f3a24f3030) | fix(@angular-devkit/build-angular): silence Sass compiler warnings from 3rd party stylesheets | +| [07763702f](https://github.com/angular/angular-cli/commit/07763702fd244ba44aebb714a295dbf5ba72b91d) | fix(@angular-devkit/build-angular): force linker `sourceMapping` option to false. | +| [a5c69722f](https://github.com/angular/angular-cli/commit/a5c69722ffeceb72dcd46901c2bb983e5dc8bf32) | fix(@angular-devkit/build-angular): ensure `NG_PERSISTENT_BUILD_CACHE` always creates a cache in the specified cache directory | +| [c65b04999](https://github.com/angular/angular-cli/commit/c65b049996a8de9d9fcc66631872424cbe5f13f9) | fix(@angular-devkit/build-angular): fail browser build when index generation fails | +| [3d71c63b3](https://github.com/angular/angular-cli/commit/3d71c63b3a11946ebfca3f0d97d4fbf8dca16255) | fix(@angular-devkit/build-angular): fix issue were `@media all` causing critical CSS inling to fail | +| [9a04975a2](https://github.com/angular/angular-cli/commit/9a04975a2170c3ecc2c09c32bd15a89c613e198f) | fix(@angular-devkit/build-angular): `extractLicenses` didn't have an effect when using server builder | +| [2ac8e9c0e](https://github.com/angular/angular-cli/commit/2ac8e9c0e131bf7fcb2c6e92500eeaa112efcefb) | fix(@angular-devkit/build-angular): display incompatibility errors | +| [2c2b49919](https://github.com/angular/angular-cli/commit/2c2b499193fb319e1c9cb92318610353b7720e2b) | fix(@angular-devkit/build-angular): limit advanced terser passes to two | +| [1be3b0783](https://github.com/angular/angular-cli/commit/1be3b07836659487e4aa9b8c71c673635e268a60) | fix(@angular-devkit/build-angular): exclude `outputPath` from persistent build cache key | +| [fefd6d042](https://github.com/angular/angular-cli/commit/fefd6d04213e61d3f48c0484d8c6a8dcff1ecd34) | perf(@angular-devkit/build-angular): use `esbuild` as a CSS optimizer for component styles | +| [18cfa0431](https://github.com/angular/angular-cli/commit/18cfa04317230f934ccba798c080543bb389725f) | feat(@angular-devkit/build-angular): add support to inline Adobe Fonts | +| [9a751f0f8](https://github.com/angular/angular-cli/commit/9a751f0f81919d67f5eeeaecbe807d5c216f6a7a) | fix(@angular-devkit/build-angular): handle `ENOENT` and `ENOTDIR` errors when deleting outputs | +| [41e645792](https://github.com/angular/angular-cli/commit/41e64579213b9d4a7c976ea45daa6b32d980df10) | fix(@angular-devkit/build-angular): downlevel `for await...of` when targeting ES2018+ | +| [070a13364](https://github.com/angular/angular-cli/commit/070a1336478d721bbbb474622f50fab455cda26c) | fix(@angular-devkit/build-angular): configure webpack target in common configuration | +| [da32daa75](https://github.com/angular/angular-cli/commit/da32daa75d08d4be177af5fa16088398d7fb427b) | perf(@angular-devkit/build-angular): use combination of `esbuild` and `terser` as a JavaScript optimizer | +| [6a2b11906](https://github.com/angular/angular-cli/commit/6a2b11906e4173562a82b3654ff662dd05513049) | perf(@angular-devkit/build-angular): cache JavaScriptOptimizerPlugin results | +| [ab17b1721](https://github.com/angular/angular-cli/commit/ab17b1721c05366e592cf805ad6d25e672b314bf) | fix(@angular-devkit/build-angular): handle ng-packagr errors more gracefully. | +| [d4c5f8518](https://github.com/angular/angular-cli/commit/d4c5f8518d4801b9fd76de289a015dcbb8d8f69b) | fix(@angular-devkit/build-angular): control linker template sourcemapping via builder sourcemap options | +| [06181c2fb](https://github.com/angular/angular-cli/commit/06181c2fbf5a20396b2d0e2b3925ceb1276947fb) | fix(@angular-devkit/build-angular): parse web-workers in tests when webWorkerTsConfig is defined | + +### @angular-devkit/build-webpack + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| [615353022](https://github.com/angular/angular-cli/commit/61535302204a2a767f85053b7efaa6ac5ac64098) | fix(@angular-devkit/build-webpack): emit result when webpack is closed | + +### @ngtools/webpack + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| [dbbcf5c8c](https://github.com/angular/angular-cli/commit/dbbcf5c8c4ec4427609942f4ef7053c1b51773c9) | fix(@ngtools/webpack): only track file dependencies | +| [7536338e0](https://github.com/angular/angular-cli/commit/7536338e0becc7f9cde62becbde58e18a270cb31) | fix(@ngtools/webpack): allow generated assets of Angular component resources | +| [720feee34](https://github.com/angular/angular-cli/commit/720feee34f910fc11c40e2f68d919d61b7d6cbec) | fix(@ngtools/webpack): avoid non-actionable template type-checker syntax diagnostics | +| [6a7bcf330](https://github.com/angular/angular-cli/commit/6a7bcf3300b459aef80fcf98f2475c977f6244dc) | fix(@ngtools/webpack): encode component style data | +| [12c14b565](https://github.com/angular/angular-cli/commit/12c14b56537d65d6986e245ab1ae4dd9aa8dd378) | fix(@ngtools/webpack): remove no longer needed component styles workaround | + +### @schematics/angular + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| [20fd33f6d](https://github.com/angular/angular-cli/commit/20fd33f6d4ce6cef1feb508a0221222e83a85630) | feat(@schematics/angular): destroy test module after every test | +| [5b10d4f54](https://github.com/angular/angular-cli/commit/5b10d4f549ebc12645ad08cba8ab7b91eaa87d28) | fix(@schematics/angular): remove unsafe any usage in application spec file | +| [1b5e18e7b](https://github.com/angular/angular-cli/commit/1b5e18e7b401efb7ec73d99c4d77d9b29e956724) | fix(@schematics/angular): replace interactive `div` with `button` in application component template | +| [0907b6941](https://github.com/angular/angular-cli/commit/0907b694174d6d684d965baf6cd37b87f49742e8) | fix(@schematics/angular): use stricter semver for `karma-jasmine-html-reporter` | +| [8ad1539c5](https://github.com/angular/angular-cli/commit/8ad1539c5e73bad30eb6eb340379d64db208098c) | fix(@schematics/angular): add 'none' value for the 'style' option of the component schematic | +| [e5ba29c7d](https://github.com/angular/angular-cli/commit/e5ba29c7d54cbd83057cf23a21119ea5a3146993) | fix(@schematics/angular): display warning during migrations when using third-party builders | +| [a44dc02fe](https://github.com/angular/angular-cli/commit/a44dc02feecaf8735f2dc6128a5b6cc5666b4434) | fix(@schematics/angular): add devtools to ng new | + +## Special Thanks: + +Alan Agius, Charles Lyding, David Scourfield, Doug Parker, hien-pham, Joey Perrott, LeonEck, Mike +Jancar, twerske, Vaibhav Singh and originalfrostig + + + + + +# 12.2.0-rc.0 (2021-07-28) + +### @angular/cli + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| [259e26979](https://github.com/angular/angular-cli/commit/259e26979ebc712ee08fd36fb68a9576c1e02447) | fix(@angular/cli): merge npmrc files values | + +### @angular-devkit/build-angular + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| [d750c686f](https://github.com/angular/angular-cli/commit/d750c686fd26f3ccfccb039027bd816a91279497) | fix(@angular-devkit/build-angular): add priority to copy-webpack-plugin patterns | + +### @angular-devkit/build-webpack + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| [615353022](https://github.com/angular/angular-cli/commit/61535302204a2a767f85053b7efaa6ac5ac64098) | fix(@angular-devkit/build-webpack): emit result when webpack is closed | + +## Special Thanks: + +Alan Agius, Charles Lyding, Joey Perrott and originalfrostig + + + + + +# 12.1.4 (2021-07-28) + +### @angular/cli + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| [e02c97dd0](https://github.com/angular/angular-cli/commit/e02c97dd09399443438b32cf1ad47fa0f7011df3) | fix(@angular/cli): merge npmrc files values | + +### @schematics/angular + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| [cfc267426](https://github.com/angular/angular-cli/commit/cfc267426716e9ecf0c9833720cb35298284f699) | fix(@schematics/angular): ensure valid SemVer range for new project Angular packages | + +### @angular-devkit/build-angular + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| [55c0bddc8](https://github.com/angular/angular-cli/commit/55c0bddc8b2425309f00733eca96c06f60f867d5) | fix(@angular-devkit/build-angular): add priority to copy-webpack-plugin patterns | + +### @angular-devkit/build-webpack + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| [b3736a3c0](https://github.com/angular/angular-cli/commit/b3736a3c09f39f5ee5dc12d98535fe4b6803ea3b) | fix(@angular-devkit/build-webpack): emit result when webpack is closed | + +## Special Thanks: + +Alan Agius, Charles Lyding, Joey Perrott and originalfrostig + + + + + +# 12.2.0-next.3 (2021-07-21) + +### @angular/cli + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| [c1eddbdc9](https://github.com/angular/angular-cli/commit/c1eddbdc98631fdfff287ce566d79ed43b601e0f) | fix(@angular/cli): handle `YARN_` environment variables during `ng update` and `ng add` | +| [6b00d1270](https://github.com/angular/angular-cli/commit/6b00d1270acaf33f32ee68c4254ce06951ddcb8c) | fix(@angular/cli): handle NPM_CONFIG environment variables during ng update and ng add | + +### @angular-devkit/build-angular + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| [4bcd1dc9e](https://github.com/angular/angular-cli/commit/4bcd1dc9ee744343a465d73d51d4a062964a3714) | fix(@angular-devkit/build-angular): allow classes with pure annotated static properties to be optimized | +| [ceade0c27](https://github.com/angular/angular-cli/commit/ceade0c27e4b8b0e731e6ca5128fd86cf071d029) | fix(@angular-devkit/build-angular): dasherize disable-host-check suggestion | + +## Special Thanks: + +Alan Agius, Charles Lyding, Joey Perrott, LeonEck and Mike Jancar + + + + + +# 12.1.3 (2021-07-21) + +### @angular/cli + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| [eaa2378b6](https://github.com/angular/angular-cli/commit/eaa2378b6bc69a2485cce742ef95b0b94ae994c6) | fix(@angular/cli): handle `YARN_` environment variables during `ng update` and `ng add` | +| [4b9a41bde](https://github.com/angular/angular-cli/commit/4b9a41bdedcdc4e115e8956d31126c5bf6f442ca) | fix(@angular/cli): handle NPM_CONFIG environment variables during ng update and ng add | + +### @angular-devkit/build-angular + +| Commit | Description | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| [04e9ffe4f](https://github.com/angular/angular-cli/commit/04e9ffe4f6b262ce5ef630310bed318e1466d238) | fix(@angular-devkit/build-angular): allow classes with pure annotated static properties to be optimized | +| [6ae17e265](https://github.com/angular/angular-cli/commit/6ae17e26547a0174f7a8910c514016db60fe4c7a) | fix(@angular-devkit/build-angular): dasherize disable-host-check suggestion | + +## Special Thanks: + +Alan Agius, Charles Lyding, Joey Perrott, LeonEck and Mike Jancar + + + + + +# v12.2.0-next.2 (2021-07-14) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.2.0-next.2)

Commit + Description + Notes +
+ + + silence Sass compiler warnings from 3rd party stylesheets + + [Closes #21235]
+
+ +
+ + + force linker `sourceMapping` option to false. + + [Closes #21271]
+
+ +
+ + + ensure `NG_PERSISTENT_BUILD_CACHE` always creates a cache in the specified cache directory +
+ + + fail browser build when index generation fails +
+ + + fix issue were `@media all` causing critical CSS inling to fail + + [Closes #20804]
+
+ +
+ + + `extractLicenses` didn't have an effect when using server builder +
+ + + display incompatibility errors + + [Closes #21322]
+
+ +
+ + + limit advanced terser passes to two +
+ + + exclude `outputPath` from persistent build cache key + + [Closes #21275]
+
+ +
+ + + use `esbuild` as a CSS optimizer for component styles +

@ngtools/webpack (12.2.0-next.2)

Commit + Description + Notes +
+ + + only track file dependencies + + [Closes #21228]
+
+ +
+ + + allow generated assets of Angular component resources +
+ + + avoid non-actionable template type-checker syntax diagnostics +

@schematics/angular (12.2.0-next.2)

Commit + Description + Notes +
+ + + destroy test module after every test + + [Closes #21280]
+
+ +
+ + + remove unsafe any usage in application spec file +
+ + + replace interactive `div` with `button` in application component template +
+ + + use stricter semver for `karma-jasmine-html-reporter` +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Joey Perrott + + + + + +# v12.1.2 (2021-07-14) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.1.2)

Commit + Description + Notes +
+ + + silence Sass compiler warnings from 3rd party stylesheets + + [Closes #21235]
+
+ +
+ + + ensure `NG_PERSISTENT_BUILD_CACHE` always creates a cache in the specified cache directory +
+ + + force linker `sourceMapping` option to false. + + [Closes #21271]
+
+ +
+ + + fail browser build when index generation fails +
+ + + `extractLicenses` didn't have an effect when using server builder +
+ + + fix issue were `@media all` causing critical CSS inling to fail + + [Closes #20804]
+
+ +
+ + + display incompatibility errors + + [Closes #21322]
+
+ +
+ + + exclude `outputPath` from persistent build cache key + + [Closes #21275]
+
+ +

@ngtools/webpack (12.1.2)

Commit + Description + Notes +
+ + + only track file dependencies + + [Closes #21228]
+
+ +
+ + + allow generated assets of Angular component resources +
+ + + avoid non-actionable template type-checker syntax diagnostics +

@schematics/angular (12.1.2)

Commit + Description + Notes +
+ + + remove unsafe any usage in application spec file +
+ + + replace interactive `div` with `button` in application component template +
+ + + use stricter semver for `karma-jasmine-html-reporter` +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Joey Perrott, Terence D. Honles + + + + + +# v12.1.1 (2021-07-01) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.1.1)

Commit + Description + Notes +
+ + + handle `ENOENT` and `ENOTDIR` errors when deleting outputs + + [Closes #21202]
+
+ +
+ + + downlevel `for await...of` when targeting ES2018+ + + [Closes #21196]
+
+ +
+ + + configure webpack target in common configuration + + [Closes #21239]
+
+ +
+ + + update `mini-css-extract-plugin` to `1.6.2` +
+ + + update `webpack` to `5.41.1` +

@angular/cli (12.1.1)

Commit + Description + Notes +
+ + + disable update notifier when retrieving package manager version during `ng version` + + [Closes #21172]
+
+ +

@ngtools/webpack (12.1.1)

Commit + Description + Notes +
+ + + encode component style data + + [Closes #21236]
+
+ +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Doug Parker + + + + + +# v12.2.0-next.1 (2021-07-01) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.2.0-next.1)

Commit + Description + Notes +
+ + + add support to inline Adobe Fonts + + [Closes #21186]
+
+ +
+ + + handle `ENOENT` and `ENOTDIR` errors when deleting outputs + + [Closes #21202]
+
+ +
+ + + downlevel `for await...of` when targeting ES2018+ + + [Closes #21196]
+
+ +
+ + + configure webpack target in common configuration + + [Closes #21239]
+
+ +
+ + + use combination of `esbuild` and `terser` as a JavaScript optimizer +
+ + + cache JavaScriptOptimizerPlugin results +

@angular/cli (12.2.0-next.1)

Commit + Description + Notes +
+ + + disable update notifier when retrieving package manager version during `ng version` + + [Closes #21172]
+
+ +

@ngtools/webpack (12.2.0-next.1)

Commit + Description + Notes +
+ + + encode component style data + + [Closes #21236]
+
+ +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Doug Parker + + + + + +# v12.2.0-next.0 (2021-06-24) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.1.0)

Commit + Description + Notes +
+ + + handle ng-packagr errors more gracefully. +
+ + + control linker template sourcemapping via builder sourcemap options +
+ + + parse web-workers in tests when webWorkerTsConfig is defined +

@ngtools/webpack (12.1.0)

Commit + Description + Notes +
+ + + remove no longer needed component styles workaround +

@schematics/angular (12.1.0)

Commit + Description + Notes +
+ + + add 'none' value for the 'style' option of the component schematic +
+ + + display warning during migrations when using third-party builders +
+ + + add devtools to ng new +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Doug Parker, Vaibhav Singh, Joey Perrott, twerske, David Scourfield, hien-pham + + + + + +# v12.1.0 (2021-06-24) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.1.0)

Commit + Description + Notes +
+ + + enable webpack Trusted Types support +
+ + + deprecate protractor builder +
+ + + support using TypeScript 4.3 +
+ + + revert open to 8.0.2 + + [Closes #20807]
+
+ +
+ + + correctly ignore inline styles during i18n extraction +
+ + + use the name as chunk filename instead of id +
+ + + handle ng-packagr errors more gracefully. +
+ + + control linker template sourcemapping via builder sourcemap options +
+ + + parse web-workers in tests when webWorkerTsConfig is defined +
+ + + use CSS optimization plugin that leverages workers +
+ + + enable opt-in usage of file system cache +

@angular/cli (12.1.0)

Commit + Description + Notes +
+ + + show Node.js version support status in version command + + [Closes #20879]
+
+ +
+ + + handle unscoped authentication details in `.npmrc` files +
+ + + don't resolve `.npmrc` from parent directories +

@ngtools/webpack (12.1.0)

Commit + Description + Notes +
+ + + support using TypeScript 4.3 +
+ + + remove redundant inline style cache +
+ + + ensure plugin provided Webpack instance is used +
+ + + disable caching for ngcc synchronous Webpack resolver +
+ + + remove no longer needed component styles workaround +

@schematics/angular (12.1.0)

Commit + Description + Notes +
+ + + create new projects with TypeScript 4.3 +
+ + + add migration to replace deprecated `--prod` + + [Closes #21036]
+
+ +
+ + + add 'none' value for the 'style' option of the component schematic +
+ + + display warning during migrations when using third-party builders +
+ + + add devtools to ng new +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Doug Parker, Joey Perrott, Bjarki, Vaibhav Singh, twerske, David Scourfield, hien-pham, Alberto Calvo, Paul Gschwendtner, Keen Yee Liau + + + + + +# v12.1.0-next.6 (2021-06-17) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.1.0-next.6)

Commit + Description + Notes +
+ + + don't parse `new Worker` syntax when `webWorkerTsConfig` is not defined in karma builder + + [Closes #21108]
+
+ +
+ + + explicitly set compilation target in test configuration + + [Closes #21111]
+
+ +
+ + + use the name as chunk filename instead of id +
+ + + enable opt-in usage of file system cache +

@angular/cli (12.1.0-next.6)

Commit + Description + Notes +
+ + + handle unscoped authentication details in `.npmrc` files +
+ + + don't resolve `.npmrc` from parent directories +

@schematics/angular (12.1.0-next.6)

Commit + Description + Notes +
+ + + add migration to replace deprecated `--prod` + + [Closes #21036]
+
+ +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Joey Perrott, Alberto Calvo, Charles Lyding + + + + + +# v12.0.5 (2021-06-17) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.0.5)

Commit + Description + Notes +
+ + + don't parse `new Worker` syntax when `webWorkerTsConfig` is not defined in karma builder + + [Closes #21108]
+
+ +
+ + + explicitly set compilation target in test configuration + + [Closes #21111]
+
+ +

@angular/cli (12.0.5)

Commit + Description + Notes +
+ + + handle unscoped authentication details in .npmrc files +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Joey Perrott + + + + + +# v12.1.0-next.5 (2021-06-10) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.1.0-next.5)

Commit + Description + Notes +
+ + + support using TypeScript 4.3 +
+ + + ensure all Webpack Stats assets are present on rebuilds + + [Closes #21038]
+
+ +
+ + + dispose Sass worker resources on Webpack shutdown + + [Closes #20985]
+
+ +
+ + + show progress during re-builds +
+ + + correctly mark async chunks as non initial in dev-server +
+ + + add web-workers in lazy chunks in stats output + + [Closes #21059]
+
+ +
+ + + styles CSS files not available in unit tests + + [Closes #21054]
+
+ +
+ + + reduce memory usage by cleaning output directory before emitting +

@angular-devkit/schematics (12.1.0-next.5)

Commit + Description + Notes +
+ + + handle updating renamed files + + [Closes #14255]
+
+ + + [Closes #21083]
+
+ +

@angular/cli (12.1.0-next.5)

Commit + Description + Notes +
+ + + avoid shell exec when bootstrapping update command +
+ + + correctly redirect nested Angular schematic dependency requests + + [Closes #21075]
+
+ +

@ngtools/webpack (12.1.0-next.5)

Commit + Description + Notes +
+ + + support using TypeScript 4.3 +
+ + + ensure plugin provided Webpack instance is used +
+ + + disable caching for ngcc synchronous Webpack resolver +

@schematics/angular (12.1.0-next.5)

Commit + Description + Notes +
+ + + create new projects with TypeScript 4.3 +
+ + + added webWorkerTsConfig into test option +
+ + + working with formatting +
+ +--- + +--- + +# Special Thanks + +Charles Lyding, Alan Agius, Doug Parker, Santosh Mahto, Joey Perrott + + + + + +# v12.0.4 (2021-06-09) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.0.4)

Commit + Description + Notes +
+ + + ensure all Webpack Stats assets are present on rebuilds + + [Closes #21038]
+
+ +
+ + + dispose Sass worker resources on Webpack shutdown + + [Closes #20985]
+
+ +
+ + + show progress during re-builds +
+ + + correctly mark async chunks as non initial in dev-server +
+ + + add web-workers in lazy chunks in stats output + + [Closes #21059]
+
+ +
+ + + styles CSS files not available in unit tests + + [Closes #21054]
+
+ +
+ + + reduce memory usage by cleaning output directory before emitting +

@angular-devkit/schematics (12.0.4)

Commit + Description + Notes +
+ + + handle updating renamed files + + [Closes #14255]
+
+ + + [Closes #21083]
+
+ +

@angular/cli (12.0.4)

Commit + Description + Notes +
+ + + avoid shell exec when bootstrapping update command +
+ + + correctly redirect nested Angular schematic dependency requests + + [Closes #21075]
+
+ +

@ngtools/webpack (12.0.4)

Commit + Description + Notes +
+ + + ensure plugin provided Webpack instance is used +

@schematics/angular (12.0.4)

Commit + Description + Notes +
+ + + added webWorkerTsConfig into test option +
+ + + working with formatting +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Santosh Mahto, Joey Perrott, Doug Parker + + + + + +# v12.0.3 (2021-06-02) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.0.3)

Commit + Description + Notes +
+ + + do not resolve web-workers in server builds + + [Closes #20877]
+
+ +
+ + + provided earlier build feedback in console + + [Closes #20957]
+
+ +
+ + + correctly ignore inline styles during i18n extraction + + [Closes #20968]
+
+ +
+ + + update `license-webpack-plugin` to `2.3.19` +

@angular-devkit/build-webpack (0.1200.3)

Commit + Description + Notes +
+ + + include only required stats in webpackStats +

@angular-devkit/core (12.0.3)

Commit + Description + Notes +
+ + + show allowed enum values when validation on enum fails +
+ + + handle complex smart defaults in schemas +
+ + + handle async schema validations +
+ + + transform path using getSystemPath for NodeJsAsyncHost's `exists` method +

@angular/cli (12.0.3)

Commit + Description + Notes +
+ + + update supported range of node versions to be less restrictive + + [Closes #20796]
+
+ +

@ngtools/webpack (12.0.3)

Commit + Description + Notes +
+ + + normalize paths when adding file dependencies + + [Closes #20891]
+
+ +
+ + + remove redundant inline style cache +

@schematics/angular (12.0.3)

Commit + Description + Notes +
+ + + make version 12 workspace config migration idempotent + + [Closes #20979]
+
+ +
+ + + show better error when non existing project is passed to the component schematic +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Doug Parker, Charles Lyding, why520crazy + + + + + +# v12.1.0-next.4 (2021-06-02) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.1.0-next.4)

Commit + Description + Notes +
+ + + do not resolve web-workers in server builds + + [Closes #20877]
+
+ +
+ + + provided earlier build feedback in console + + [Closes #20957]
+
+ +
+ + + correctly ignore inline styles during i18n extraction +

@angular-devkit/build-webpack (0.1201.0-next.4)

Commit + Description + Notes +
+ + + include only required stats in webpackStats +

@angular-devkit/core (12.1.0-next.4)

Commit + Description + Notes +
+ + + show allowed enum values when validation on enum fails +
+ + + handle complex smart defaults in schemas +
+ + + handle async schema validations +
+ + + transform path using getSystemPath for NodeJsAsyncHost's `exists` method +

@angular/cli (12.1.0-next.4)

Commit + Description + Notes +
+ + + update supported range of node versions to be less restrictive + + [Closes #20796]
+
+ +

@ngtools/webpack (12.1.0-next.4)

Commit + Description + Notes +
+ + + normalize paths when adding file dependencies + + [Closes #20891]
+
+ +
+ + + remove redundant inline style cache +

@schematics/angular (12.1.0-next.4)

Commit + Description + Notes +
+ + + make version 12 workspace config migration idempotent + + [Closes #20979]
+
+ +
+ + + show better error when non existing project is passed to the component schematic +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Doug Parker, Charles Lyding, why520crazy + + + + + +# v12.1.0-next.3 (2021-05-26) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.1.0-next.3)

Commit + Description + Notes +
+ + + enable webpack Trusted Types support +
+ + + deprecate protractor builder +
+ + + ensure Sass worker implementation supports Node.js 12.14 +
+ + + don't add `.hot-update.js` script tags + + [Closes #20855]
+
+ +
+ + + correctly generate ServiceWorker config on Windows + + [Closes #20894]
+
+ +
+ + + ensure latest inline stylesheet data is used during rebuilds +
+ + + allow i18n extraction on application that uses web-workers + + [Closes #20930]
+
+ +
+ + + hide stacktraces from dart-sass errors +
+ + + resolve absolute outputPath properly + + [Closes #20935]
+
+ +
+ + + show `--disable-host-check` warning only when not using `disableHostCheck` + + [Closes #20951]
+
+ +
+ + + disable CSS optimization parallelism for components styles + + [Closes #20883]
+
+ +
+ + + load postcss-preset-env configuration once +

@angular/cli (12.1.0-next.3)

Commit + Description + Notes +
+ + + show Node.js version support status in version command + + [Closes #20879]
+
+ +
+ + + ng update on windows to allow path +

@ngtools/webpack (12.1.0-next.3)

Commit + Description + Notes +
+ + + re-emit component stylesheet assets + + [Closes #20882]
+
+ +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Doug Parker, Bjarki, Hassan Sani, JoostK, George Kalpakas, Joey Perrott + + + + + +# v12.0.2 (2021-05-26) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.0.2)

Commit + Description + Notes +
+ + + ensure Sass worker implementation supports Node.js 12.14 +
+ + + don't add `.hot-update.js` script tags + + [Closes #20855]
+
+ +
+ + + correctly generate ServiceWorker config on Windows + + [Closes #20894]
+
+ +
+ + + ensure latest inline stylesheet data is used during rebuilds +
+ + + allow i18n extraction on application that uses web-workers + + [Closes #20930]
+
+ +
+ + + hide stacktraces from dart-sass errors +
+ + + resolve absolute outputPath properly + + [Closes #20935]
+
+ +
+ + + show `--disable-host-check` warning only when not using `disableHostCheck` + + [Closes #20951]
+
+ +
+ + + update PostCSS to 8.3 +
+ + + disable CSS optimization parallelism for components styles + + [Closes #20883]
+
+ +
+ + + load postcss-preset-env configuration once +

@angular/cli (12.0.2)

Commit + Description + Notes +
+ + + ng update on windows to allow path +

@ngtools/webpack (12.0.2)

Commit + Description + Notes +
+ + + re-emit component stylesheet assets + + [Closes #20882]
+
+ +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Doug Parker, Hassan Sani, JoostK, George Kalpakas, Joey Perrott + + + + + +# v12.0.1 (2021-05-19) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.0.1)

Commit + Description + Notes +
+ + + add experimental web-assembly + + [Closes #20762]
+
+ +
+ + + fix error with inline styles when running extract-i18n +
+ + + add `NG_BUILD_MAX_WORKERS` settimgs to control maximum number of workers +
+ + + non injected styles should not count as initial + + [Closes #20781]
+
+ +
+ + + revert open to 8.0.2 + + [Closes #20807]
+
+ +
+ + + correctly resolve babel runtime helpers + + [Closes #20800]
+
+ +
+ + + compile schema in synchronously + + [Closes #20847]
+
+ +
+ + + execute dart-sass in a worker +
+ + + reduce JSON stats +
+ + + use CSS optimization plugin that leverages workers +
+ + + render Sass using a pool of workers +
+ + + clean no-longer used assets during builds +

@angular/cli (12.0.1)

Commit + Description + Notes +
+ + + cannot locate bin for temporary package +
+ + + clean node modules directory prior to updating +
+ + + improve `--prod` deprecation warning + + [Closes #20806]
+
+ +

@ngtools/webpack (12.0.1)

Commit + Description + Notes +
+ + + reduce non-watch mode TypeScript diagnostic analysis overhead +

@schematics/angular (12.0.1)

Commit + Description + Notes +
+ + + remove --prod option from README template +
+ + + don't add `skipTest` option to module schematic options + + [Closes #20811]
+
+ +
+ + + add migration to remove `skipTests` from `@schematics/angular:module` + + [Closes #20848]
+
+ +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Joey Perrott, Keen Yee Liau, Luca Vazzano, Pankaj Patil, Ryan Lester, Terence D. Honles, Alan Cohen + + + + + +# v12.1.0-next.2 (2021-05-19) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.1.0-next.2)

Commit + Description + Notes +
+ + + add experimental web-assembly + + [Closes #20762]
+
+ +
+ + + add `NG_BUILD_MAX_WORKERS` settimgs to control maximum number of workers +
+ + + non injected styles should not count as initial + + [Closes #20781]
+
+ +
+ + + revert open to 8.0.2 + + [Closes #20807]
+
+ +
+ + + correctly resolve babel runtime helpers + + [Closes #20800]
+
+ +
+ + + compile schema in synchronously + + [Closes #20847]
+
+ +
+ + + execute dart-sass in a worker +
+ + + reduce JSON stats +
+ + + use CSS optimization plugin that leverages workers +
+ + + render Sass using a pool of workers +
+ + + clean no-longer used assets during builds +

@angular/cli (12.1.0-next.2)

Commit + Description + Notes +
+ + + cannot locate bin for temporary package +
+ + + clean node modules directory prior to updating +
+ + + improve `--prod` deprecation warning + + [Closes #20806]
+
+ +

@ngtools/webpack (12.1.0-next.2)

Commit + Description + Notes +
+ + + reduce non-watch mode TypeScript diagnostic analysis overhead +

@schematics/angular (12.1.0-next.2)

Commit + Description + Notes +
+ + + remove --prod option from README template +
+ + + don't add `skipTest` option to module schematic options + + [Closes #20811]
+
+ +
+ + + add migration to remove `skipTests` from `@schematics/angular:module` + + [Closes #20848]
+
+ +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Joey Perrott, Keen Yee Liau, Luca Vazzano, Pankaj Patil, Ryan Lester, Alan Cohen, Paul Gschwendtner + + + + + +# v12.0.0 (2021-05-12) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/architect (0.1200.0)

Commit + Description + Notes +
+ + + add implementation for defaultConfiguration +

@angular-devkit/build-angular (12.0.0)

Commit + Description + Notes +
+ + + add `postcss-preset-env` with stage 3 features +
+ + + drop support for karma version 5.2 +
+ + + drop support for ng-packagr version 11 +
+ + + enable inlineCritical by default +
+ + + show warning during build when project requires IE 11 support +
+ + + expose legacy-migrate message format +
+ + + integrate JIT mode linker + + [Closes #20281]
+
+ +
+ + + upgrade to Webpack 5 throughout the build system +
+ + + support processing component inline CSS styles +
+ + + support specifying stylesheet language for inline component styles +
+ + + remove left-over `experimentalRollupPass` option +
+ + + support writing large Webpack stat outputs +
+ + + ensure output directory is present before writing stats JSON +
+ + + remove deprecated View Engine support for i18n extraction +
+ + + remove usage of deprecated View Engine compiler +
+ + + remove deprecated i18nLocale and i18nFormat options from i18n-extract +
+ + + update karma builder to use non-deprecated API +
+ + + disable webpack cache when using `NG_BUILD_CACHE` +
+ + + remove duplicate application bundle generation complete message +
+ + + mark programmatic builder execution functions as experimental +
+ + + avoid double build optimizer processing +
+ + + replace Webpack 4 `hashForChunk` hook usage +
+ + + use new Webpack watch API in karma webpack plugin +
+ + + recover from CSS optimization errors +
+ + + disable Webpack 5 automatic public path support +
+ + + always inject live reload client when using live reload +
+ + + change several builder options defaults +
+ + + show warning when using stylus +
+ + + avoid triggering file change after file build +
+ + + remove left-over `forkTypeChecker` option +
+ + + disable CSS declaration sorting optimizations + + [Closes #20693]
+
+ +
+ + + disable `showCircularDependencies` by default +
+ + + use Webpack's GC memory caching in watch mode +
+ + + improve incremental time during Karma tests +
+ + + avoid async downlevel for known ES2015 code +

@angular-devkit/build-optimizer (0.1200.0)

Commit + Description + Notes +
+ + + support Webpack 5 +

@angular-devkit/build-webpack (0.1200.0)

Commit + Description + Notes +
+ + + provide output path in builder results +
+ + + support Webpack 5 +

@angular-devkit/core (12.0.0)

Commit + Description + Notes +
+ + + add handling for `defaultConfiguration` target definition property +
+ + + update schema validator +
+ + + ensure job input values are processed in order +
+ + + improve handling of set schema values + + [Closes #20594]
+
+ +

@angular/cli (12.0.0)

Commit + Description + Notes +
+ + + add `defaultConfiguration` property to architect schema +
+ + + deprecate `--prod` command line argument +
+ + + confirm ng add action before installation +
+ + + support TypeScript 4.2 +
+ + + ensure odd number Node.js version message is a warning +
+ + + remove npm 7 incompatibility notification +
+ + + avoid exceptions for expected errors in architect commands +
+ + + ensure update migrations are fully executed +
+ + + exclude deprecated packages with removal migrations from update +
+ + + add message update updating from non LTS versions of the CLI +
+ + + ignore `tsickle` during updates +
+ + + run all migrations when updating from or between prereleases +
+ + + add package manager name and version in `ng version` output +
+ + + Support XDG Base Directory Specification +
+ + + don't display options multiple times in schematics help output +
+ + + change package installation to async +
+ + + infer schematic defaults correctly when using `--project` + + [Closes #20666]
+
+ +
+ + + propagate update's force option to package managers +
+ + + allow unsetting config when value is `undefined` +
+ + + allow config object to be of JSON. +
+ + + disallow additional properties in builders sections +

@ngtools/webpack (12.0.0)

Commit + Description + Notes +
+ + + support Webpack 5 +
+ + + drop support for string based lazy loading +
+ + + support multiple plugin instances per compilation +
+ + + support generating data URIs for inline component styles in JIT +
+ + + support processing inline component styles in AOT +
+ + + remove Webpack 5 deprecation warning in resource loader +
+ + + use correct Webpack asset stage in resource loader +
+ + + remove Webpack plugin for deprecated ViewEngine compiler +
+ + + only track actual resource file dependencies +
+ + + avoid adding transitive dependencies to Webpack's dependency graph +
+ + + use precalculated dependencies in unused file check +
+ + + only check affected files for Angular semantic diagnostics +
+ + + cache results of processed inline resources +
+ + + rebuild Angular required files asynchronously +
+ + + reduce source file and Webpack module iteration +

@schematics/angular (12.0.0)

Commit + Description + Notes +
+ + + add migration to remove deprecated options from 'angular.json' +
+ + + strict mode by default +
+ + + use new zone.js entry-points +
+ + + add migration to use new zone.js entry-points +
+ + + add migration to remove emitDecoratorMetadata +
+ + + augment `universal` schematics to import `platform-server` shims + + [Closes #40559]
+
+ +
+ + + update new project dependencies version + + [Closes #20106]
+
+ +
+ + + production builds by default +
+ + + deprecate `legacyBrowsers` application and ng-new option +
+ + + add migration to remove `lazyModules` configuration option +
+ + + add migration to update lazy loading string syntax to use dynamic imports +
+ + + update several TypeScript compilation target (Syntax) +
+ + + remove tslint and codelyzer from new projects + + [Closes #20105]
+
+ + + [Closes #18465]
+
+ +
+ + + add production by default optional migration +
+ + + update new workspaces to use Karma 6.3 +
+ + + remove `entryComponent` from `component` schematic +
+ + + configure new libraries to be published in Ivy partial mode +
+ + + update `jasmine-spec-reporter` to version 7 +
+ + + migrate web workers to support Webpack 5 +
+ + + only update removed v12 options in migration +
+ + + add `additionalProperties` to all schemas +
+ + + remove references to the prod flag +
+ + + only show legacy browsers deprecation warning when option is used +
+ + + remove leftover workspace tslint config +
+ + + correctly handle adding multi-line strings to `@NgModule` metadata +
+ + + run update-i18n migration for server builder +
+ + + update web-worker to support Webpack 5 +
+ + + set `inlineStyleLanguage` when application `style` option is used +
+ + + set `inlineStyleLanguage` for universal if present in build options +
+ + + remove jasmine-spec-reporter and ts-node from default workspace +
+ + + remove Protractor from home page +
+ + + remove lint command from package.json + + [Closes #20618]
+
+ +
+ + + fix migration for namedChunks and option +
+ + + add "type" option in enum schematic +
+ + + only run `emitDecoratorMetadata` removal migration in safe workspaces +
+ + + replace `clientProject` with `project` +
+ +--- + +  + +# Breaking Changes + +

+ @schematics/angular: remove `stylus` from `style` options (fd729ac) +

+`styl` (Stylus) is no longer a supported value as `style` in `application`, `component`, `ng-new` schematics. Stylus is not actively maintained and only 0.3% of the Angular CLI users use it. + +(cherry picked from commit 0272fc55b67d1a3f986b996c8eb21aea31eedf51) + +

+ @angular-devkit/build-angular: change several builder options defaults (656f8d7) +

+A number of browser and server builder options have had their default values changed. The aim of these changes is to reduce the configuration complexity and support the new "production builds by default" initiative. + +**Browser builder** +| Option | Previous default value | New default value | +|----------------------------------------|---------------------------|-------------------| +| optimization | false | true | +| aot | false | true | +| buildOptimizer | false | true | +| sourceMap | true | false | +| extractLicenses | false | true | +| namedChunks | true | false | +| vendorChunk | true | false | + +**Server builder** +| Option | Previous default value | New default value | +|---------------|------------------------|-------------------| +| optimization | false | true | +| sourceMap | true | false | + +(cherry picked from commit 0a74d0d28daf68510459ed73ef048c91bfcabbbc) + +

+ @angular-devkit/core: update schema validator (0875313) +

+support for JSON Schema draft-04 and draft-06 is removed. If you have schemas using the `id` keyword replace them with `$id`. For an interim period we will auto rename any top level `id` keyword to `$id`. + +**NB**: This change only effects schematics and builders authors. + +

+ @angular-devkit/build-angular: upgrade to Webpack 5 throughout the build system (d883ce5) +

+Webpack 5 lazy loaded file name changes +Webpack 5 generates similar but differently named files for lazy loaded JavaScript files in development configurations (when the `namedChunks` option is enabled). +For the majority of users this change should have no effect on the application and/or build process. Production builds should also not be affected as the `namedChunks` option is disabled by default in production configurations. +However, if a project's post-build process makes assumptions as to the file names then adjustments may need to be made to account for the new naming paradigm. +Such post-build processes could include custom file transformations after the build, integration into service-side frameworks, or deployment procedures. +Example development file name change: `lazy-lazy-module.js` --> `src_app_lazy_lazy_module_ts.js` + +Webpack 5 now includes web worker support. However, the structure of the URL within the `Worker` constructor must be in a specific format that differs from the current requirement. +Web worker usage should be updated as shown below (where `./app.worker` should be replaced with the actual worker name): +Before: `new Worker('./app.worker', ...)` +After: `new Worker(new URL('./app.worker', import.meta.url), ...)` + +

+ @ngtools/webpack: remove Webpack plugin for deprecated ViewEngine compiler (160102a) +

+Removal of View Engine support from application builds +With the removal of the deprecated View Engine compiler in Angular version 12 for applications, the View Engine Webpack plugin has been removed. +The Ivy-based Webpack plugin is the default used within the Angular CLI. +If using a custom standalone Webpack configuration, the removed `AngularCompilerPlugin` should be replaced with the Ivy-based `AngularWebpackPlugin`. + +

+ @angular-devkit/build-angular: remove deprecated i18n options from server and browser builder (5cf9a08) +

+Removal of deprecated browser and server command options. +- `i18nFile`, use `locales` object in the project metadata instead. +- `i18nFormat`, No longer needed as the format will be determined automatically. +- `i18nLocale`, use `localize` option instead. + +

+ @angular-devkit/build-angular: remove deprecated i18nLocale and i18nFormat options from i18n-extract (eca5a01) +

+Removal of deprecated `extract-i18n` command options +The deprecated `i18nLocale` option has been removed and the `i18n.sourceLocale` within a project's configuration should be used instead. +The deprecated `i18nFormat` option has been removed and the `format` option should be used instead. + +

+ @angular-devkit/build-angular: remove usage of deprecated View Engine compiler (677913f) +

+Removal of View Engine support from application builds +With the removal of the deprecated View Engine compiler in Angular version 12 for applications, Ivy-based compilation will always be used when building an application. +The default behavior for applications is to use the Ivy compiler when building and no changes are required for these applications. +For applications that have opted-out of Ivy, a warning will be shown and an Ivy-based build will be attempted. If the build fails, +the application may need to be updated to become Ivy compatible. + +

+ @schematics/angular: remove `entryComponent` from `component` schematic (8582ddc) +

+`entryComponent` option has been removed from the `component` schematic as this was intended to be used with the now no longer supported ViewEngine rendering engine. + +

+ @angular-devkit/build-angular: remove view engine app-shell generation (1c2aeeb) +

+App-shell builder now only supports generation using Ivy + +

+ @angular-devkit/build-angular: remove deprecated View Engine support for i18n extraction (012700a) +

+Removal of View Engine support from i18n extraction +With the removal of the deprecated View Engine compiler in Angular version 12 for applications, the `ng extract-i18n` command will now always use the Ivy compiler. +The `--ivy` option has also been removed as Ivy-based extraction is always enabled. +The default behavior for applications is to use the Ivy compiler for building/extraction and no changes are required for these applications. +For applications that have opted-out of Ivy, a warning will be shown and Ivy-based extraction will be attempted. If the extraction fails, +the application may need to be updated to become Ivy compatible. + +

+ @angular/cli: confirm ng add action before installation (985dc1a) +

+The `ng add` command will now ask the user to confirm the package and version prior to installing and executing an uninstalled package. +This new behavior allows a user to abort the action if the version selected is not appropriate or if a typo occurred on the command line and an incorrect package would be installed. +A `--skip-confirmation` option has been added to skip the prompt and directly install and execute the package. This option is useful in CI and non-TTY scenarios such as automated scripts. + +

+ @angular-devkit/build-angular: remove deprecated `lazyModules` option (8d66912) +

+Server and Browser builder `lazyModules` option has been removed without replacement. + +

+ @ngtools/webpack: drop support for string based lazy loading (0dc7327) +

+With this change we drop support for string based lazy loading `./lazy.module#LazyModule` use dynamic imports instead. + +The following options which were used to support the above syntax were removed without replacement. + +- discoverLazyRoutes +- additionalLazyModules +- additionalLazyModuleResources +- contextElementDependencyConstructor + +

+ @angular-devkit/build-angular: enable inlineCritical by default (aa3ea88) +

+Critical CSS inlining is now enabled by default. If you wish to turn this off set `inlineCritical` to `false`. + +See: https://angular.dev/reference/configs/workspace-config#optimization-configuration + +

+ @angular-devkit/build-angular: drop support for zone.js 0.10 (f309516) +

+Minimum supported `zone.js` version is `0.11.4` + +

+ @angular-devkit/build-angular: drop support for ng-packagr version 11 (44e75be) +

+Minimum supported `ng-packagr` version is `12.0.0-next` + +

+ @angular-devkit/build-angular: drop support for karma version 5.2 (fa5cf53) +

+Minimum supported `karma` version is `6.0.0` + +

+ set minimum Node.js version to 12.13 (d1f6169) +

+Node.js version 10 will become EOL on 2021-04-30. +Angular CLI 12 will require Node.js 12.13+ or 14.15+. Node.js 12.13 and 14.15 are the first LTS releases for their respective majors. + +

+ @angular-devkit/build-angular: remove file-loader dependency (6732294) +

+The unsupported/undocumented, Webpack specific functionality to `import`/`require()` a non-module file has been removed. + +Before + +```js +import img from './images/asset.png'; +``` + +After + +```html + +``` + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Keen Yee Liau, Joey Perrott, Doug Parker, Cédric Exbrayat, Douglas Parker, George Kalpakas, Sam Bulatov, Joshua Chapman, Santosh Yadav, David Shevitz, Kristiyan Kostadinov + + + + + +# v12.0.0-rc.3 (2021-05-10) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular/cli (12.0.0-rc.3)

Commit + Description + Notes +
+ + + propagate update's force option to package managers +
+ + + allow unsetting config when value is `undefined` +
+ + + allow config object to be of JSON. +
+ + + disallow additional properties in builders sections +
+ +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Joey Perrott + + + + + +# v12.0.0-rc.2 (2021-05-05) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.0.0-rc.2)

Commit + Description + Notes +
+ + + disable CSS declaration sorting optimizations + + [Closes #20693]
+
+ +

@angular/cli (12.0.0-rc.2)

Commit + Description + Notes +
+ + + don't display options multiple times in schematics help output +
+ + + change package installation to async +
+ + + infer schematic defaults correctly when using `--project` + + [Closes #20666]
+
+ +

@ngtools/webpack (12.0.0-rc.2)

Commit + Description + Notes +
+ + + rebuild Angular required files asynchronously +
+ + + reduce source file and Webpack module iteration +

@schematics/angular (12.0.0-rc.2)

Commit + Description + Notes +
+ + + add "type" option in enum schematic +
+ + + only run `emitDecoratorMetadata` removal migration in safe workspaces +
+ + + replace `clientProject` with `project` +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Keen Yee Liau, Sam Bulatov, Doug Parker + + + + + +# v12.0.0-rc.1 (2021-04-28) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.0.0-rc.1)

Commit + Description + Notes +
+ + + remove left-over `forkTypeChecker` option +
+ + + output webpack-dev-server and webpack-dev-middleware errors +
+ + + improve incremental time during Karma tests +
+ + + avoid async downlevel for known ES2015 code +

@angular-devkit/core (12.0.0-rc.1)

Commit + Description + Notes +
+ + + improve handling of set schema values + + [Closes #20594]
+
+ +

@angular/cli (12.0.0-rc.1)

Commit + Description + Notes +
+ + + add package manager name and version in `ng version` output +
+ + + Support XDG Base Directory Specification +

@schematics/angular (12.0.0-rc.1)

Commit + Description + Notes +
+ + + remove jasmine-spec-reporter and ts-node from default workspace +
+ + + remove Protractor from home page +
+ + + remove lint command from package.json + + [Closes #20618]
+
+ +
+ + + avoid unuse imports for canLoad guard generation +
+ + + fix migration for namedChunks and option +

@angular-devkit/schematics-cli (12.0.0-rc.1)

Commit + Description + Notes +
+ + + accept windows like paths for schematics +
+ +--- + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Joey Perrott, Cédric Exbrayat, Doug Parker, Joshua Chapman, Billy Lando, Santosh Yadav, mzocateli + + + + + +# v12.0.0-rc.0 (2021-04-21) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.0.0-rc.0)

Commit + Description + Notes +
+ + + avoid double build optimizer processing +
+ + + replace Webpack 4 `hashForChunk` hook usage +
+ + + use new Webpack watch API in karma webpack plugin +
+ + + recover from CSS optimization errors +
+ + + disable Webpack 5 automatic public path support +
+ + + always inject live reload client when using live reload +
+ + + change several builder options defaults +
+ + + show warning when using stylus +
+ + + set Tailwind CSS mode when using Tailwind +
+ + + avoid triggering file change after file build +
+ + + use Webpack's GC memory caching in watch mode +

@angular/cli (12.0.0-rc.0)

Commit + Description + Notes +
+ + + ignore `tsickle` during updates +
+ + + run all migrations when updating from or between prereleases +

@ngtools/webpack (12.0.0-rc.0)

Commit + Description + Notes +
+ + + only track actual resource file dependencies +
+ + + cache results of processed inline resources +

@schematics/angular (12.0.0-rc.0)

Commit + Description + Notes +
+ + + set `inlineStyleLanguage` when application `style` option is used +
+ + + set `inlineStyleLanguage` for universal if present in build options +
+ +--- + +# Breaking Changes + +

+ @schematics/angular: remove `stylus` from `style` options (fd729ac) +

+`styl` (Stylus) is no longer a supported value as `style` in `application`, `component`, `ng-new` schematics. Stylus is not actively maintained and only 0.3% of the Angular CLI users use it. + +(cherry picked from commit 0272fc55b67d1a3f986b996c8eb21aea31eedf51) + +

+ @angular-devkit/build-angular: change several builder options defaults (656f8d7) +

+A number of browser and server builder options have had their default values changed. The aim of these changes is to reduce the configuration complexity and support the new "production builds by default" initiative. + +**Browser builder** +| Option | Previous default value | New default value | +|----------------------------------------|---------------------------|-------------------| +| optimization | false | true | +| aot | false | true | +| buildOptimizer | false | true | +| sourceMap | true | false | +| extractLicenses | false | true | +| namedChunks | true | false | +| vendorChunk | true | false | + +**Server builder** +| Option | Previous default value | New default value | +|---------------|------------------------|-------------------| +| optimization | false | true | +| sourceMap | true | false | + +(cherry picked from commit 0a74d0d28daf68510459ed73ef048c91bfcabbbc) + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Keen Yee Liau, Joey Perrott, David Shevitz + + + + + +# v12.0.0-next.9 (2021-04-14) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (12.0.0-next.9)

Commit + Description + Notes +
+ + + upgrade to Webpack 5 throughout the build system +
+ + + support processing component inline CSS styles +
+ + + support specifying stylesheet language for inline component styles +
+ + + update karma builder to use non-deprecated API +
+ + + disable webpack cache when using `NG_BUILD_CACHE` +
+ + + remove duplicate application bundle generation complete message +
+ + + mark programmatic builder execution functions as experimental +

@angular-devkit/build-webpack (0.1200.0-next.9)

Commit + Description + Notes +
+ + + support Webpack 5 +

@angular-devkit/core (12.0.0-next.9)

Commit + Description + Notes +
+ + + update schema validator +

@angular/cli (12.0.0-next.9)

Commit + Description + Notes +
+ + + add message update updating from non LTS versions of the CLI +

@ngtools/webpack (12.0.0-next.9)

Commit + Description + Notes +
+ + + support multiple plugin instances per compilation +
+ + + support generating data URIs for inline component styles in JIT +
+ + + support processing inline component styles in AOT +

@schematics/angular (12.0.0-next.9)

Commit + Description + Notes +
+ + + configure new libraries to be published in Ivy partial mode +
+ + + update `jasmine-spec-reporter` to version 7 +
+ + + migrate web workers to support Webpack 5 +
+ + + update web-worker to support Webpack 5 +
+ +--- + +# Breaking Changes + +

+ @angular-devkit/core: update schema validator (0875313) +

+support for JSON Schema draft-04 and draft-06 is removed. If you have schemas using the `id` keyword replace them with `$id`. For an interim period we will auto rename any top level `id` keyword to `$id`. + +**NB**: This change only effects schematics and builders authors. + +

+ @angular-devkit/build-angular: upgrade to Webpack 5 throughout the build system (d883ce5) +

+Webpack 5 generates similar but differently named files for lazy loaded JavaScript files in development configurations (when the `namedChunks` option is enabled). +For the majority of users this change should have no effect on the application and/or build process. Production builds should also not be affected as the `namedChunks` option is disabled by default in production configurations. +However, if a project's post-build process makes assumptions as to the file names then adjustments may need to be made to account for the new naming paradigm. +Such post-build processes could include custom file transformations after the build, integration into service-side frameworks, or deployment procedures. +Example development file name change: `lazy-lazy-module.js` --> `src_app_lazy_lazy_module_ts.js` + +

+ @angular-devkit/build-angular: upgrade to Webpack 5 throughout the build system (d883ce5) +

+Webpack 5 now includes web worker support. However, the structure of the URL within the `Worker` constructor must be in a specific format that differs from the current requirement. +Web worker usage should be updated as shown below (where `./app.worker` should be replaced with the actual worker name): + +Before: + +``` +new Worker('./app.worker', ...) +``` + +After: + +``` +new Worker(new URL('./app.worker', import.meta.url), ...) +``` + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Keen Yee Liau, Doug Parker, Douglas Parker + + + + + +# v12.0.0-next.8 (2021-04-07) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (0.1200.0-next.8)

Commit + Description + Notes +
+ + + remove deprecated i18nLocale and i18nFormat options from i18n-extract +

@ngtools/webpack (12.0.0-next.8)

Commit + Description + Notes +
+ + + remove Webpack plugin for deprecated ViewEngine compiler +

@schematics/angular (12.0.0-next.8)

Commit + Description + Notes +
+ + + run update-i18n migration for server builder +
+ +--- + +# Breaking Changes + +

+ @ngtools/webpack: remove Webpack plugin for deprecated ViewEngine compiler (160102a) +

+Removal of View Engine support from application builds +With the removal of the deprecated View Engine compiler in Angular version 12 for applications, the View Engine Webpack plugin has been removed. +The Ivy-based Webpack plugin is the default used within the Angular CLI. +If using a custom standalone Webpack configuration, the removed `AngularCompilerPlugin` should be replaced with the Ivy-based `AngularWebpackPlugin`. + +

+ @angular-devkit/build-angular: remove deprecated i18n options from server and browser builder (5cf9a08) +

+Removal of deprecated browser and server command options. +- `i18nFile`, use `locales` object in the project metadata instead. +- `i18nFormat`, No longer needed as the format will be determined automatically. +- `i18nLocale`, use `localize` option instead. + +

+ @angular-devkit/build-angular: remove deprecated i18nLocale and i18nFormat options from i18n-extract (eca5a01) +

+Removal of deprecated `extract-i18n` command options +The deprecated `i18nLocale` option has been removed and the `i18n.sourceLocale` within a project's configuration should be used instead. +The deprecated `i18nFormat` option has been removed and the `format` option should be used instead. + +--- + +# Special Thanks + +Charles Lyding, Renovate Bot, Alan Agius, Doug Parker, Joey Perrott + + + + + +# v12.0.0-next.7 (2021-04-02) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (0.1200.0-next.7)

Commit + Description + Notes +
+ + + validate scripts and styles bundleName + + [Closes #20360]
+
+ +
+ + + remove deprecated View Engine support for i18n extraction +
+ + + remove usage of deprecated View Engine compiler +

@angular/cli (12.0.0-next.7)

Commit + Description + Notes +
+ + + ensure update migrations are fully executed +
+ + + exclude deprecated packages with removal migrations from update +

@ngtools/webpack (12.0.0-next.7)

Commit + Description + Notes +
+ + + use correct Webpack asset stage in resource loader +
+ + + only check affected files for Angular semantic diagnostics +

@schematics/angular (12.0.0-next.7)

Commit + Description + Notes +
+ + + remove `entryComponent` from `component` schematic +
+ + + correctly handle adding multi-line strings to `@NgModule` metadata +
+ + + explicitly specify ServiceWorker registration strategy +
+ +--- + +# Breaking Changes + +

+ @angular-devkit/build-angular: remove usage of deprecated View Engine compiler (677913f) +

+Removal of View Engine support from application builds +With the removal of the deprecated View Engine compiler in Angular version 12 for applications, Ivy-based compilation will always be used when building an application. +The default behavior for applications is to use the Ivy compiler when building and no changes are required for these applications. +For applications that have opted-out of Ivy, a warning will be shown and an Ivy-based build will be attempted. If the build fails, +the application may need to be updated to become Ivy compatible. + +

+ @schematics/angular: remove `entryComponent` from `component` schematic (8582ddc) +

+`entryComponent` option has been removed from the `component` schematic as this was intended to be used with the now no longer supported ViewEngine rendering engine. + +

+ @angular-devkit/build-angular: remove view engine app-shell generation (1c2aeeb) +

+App-shell builder now only supports generation using Ivy + +

+ @angular-devkit/build-angular: remove deprecated View Engine support for i18n extraction (012700a) +

+Removal of View Engine support from i18n extraction +With the removal of the deprecated View Engine compiler in Angular version 12 for applications, the `ng extract-i18n` command will now always use the Ivy compiler. +The `--ivy` option has also been removed as Ivy-based extraction is always enabled. +The default behavior for applications is to use the Ivy compiler for building/extraction and no changes are required for these applications. +For applications that have opted-out of Ivy, a warning will be shown and Ivy-based extraction will be attempted. If the extraction fails, +the application may need to be updated to become Ivy compatible. + +--- + +# Special Thanks + +Charles Lyding, Alan Agius, Renovate Bot, George Kalpakas, Joey Perrott, Keen Yee Liau + + + + + +# v12.0.0-next.6 (2021-03-24) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (0.1200.0-next.6)

Commit + Description + Notes +
+ + + ensure output directory is present before writing stats JSON +

@schematics/angular (12.0.0-next.6)

Commit + Description + Notes +
+ + + add production by default optional migration +
+ + + update new workspaces to use Karma 6.3 +
+ + + remove leftover workspace tslint config +
+ +--- + +--- + +# Special Thanks + +Renovate Bot, Alan Agius, Charles Lyding, Keen Yee Liau + + + + + +# v12.0.0-next.5 (2021-03-18) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (0.1200.0-next.5)

Commit + Description + Notes +
+ + + expose legacy-migrate message format +
+ + + integrate JIT mode linker + + [Closes #20281]
+
+ +
+ + + display correct filename for bundles that are ES2016+ +
+ + + don't load an input sourcemap from file when using Babel +
+ + + support writing large Webpack stat outputs +
+ + + skip FESM2015 from `async` transformation +
+ + + remove Webpack Stats.toJson usage in analytics plugin +
+ + + remove Webpack Stats.toJson usage in karma plugin +
+ + + enforce Babel not to load sourcemaps from file +
+ + + disable `showCircularDependencies` by default +

@angular-devkit/build-webpack (0.1200.0-next.5)

Commit + Description + Notes +
+ + + provide output path in builder results +

@angular/cli (12.0.0-next.5)

Commit + Description + Notes +
+ + + confirm ng add action before installation +
+ + + support TypeScript 4.2 +
+ + + remove `project` from required properties in ng-packagr schema +

@ngtools/webpack (12.0.0-next.5)

Commit + Description + Notes +
+ + + remove Webpack 5 deprecation warning in resource loader +
+ + + avoid adding transitive dependencies to Webpack's dependency graph +
+ + + use precalculated dependencies in unused file check +

@schematics/angular (12.0.0-next.5)

Commit + Description + Notes +
+ + + update several TypeScript compilation target (Syntax) +
+ + + remove tslint and codelyzer from new projects + + [Closes #20105]
+
+ + + [Closes #18465]
+
+ +
+ + + remove references to the prod flag +
+ + + fix youtube icon margin +
+ + + only show legacy browsers deprecation warning when option is used +
+ + + remove Native value from viewEncapsulation option +
+ + + use title for svg on home page +
+ +--- + +# Breaking Changes + +

+ @angular/cli: confirm ng add action before installation (985dc1a) +

+The `ng add` command will now ask the user to confirm the package and version prior to installing and executing an uninstalled package. +This new behavior allows a user to abort the action if the version selected is not appropriate or if a typo occurred on the command line and an incorrect package would be installed. +A `--skip-confirmation` option has been added to skip the prompt and directly install and execute the package. This option is useful in CI and non-TTY scenarios such as automated scripts. + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Renovate Bot, Doug Parker, Cédric Exbrayat, Kristiyan Kostadinov, Mouad Ennaciri, Omar Hasan + + + + + +# v12.0.0-next.4 (2021-03-10) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/architect (0.1200.0-next.4)

Commit + Description + Notes +
+ + + add implementation for defaultConfiguration +

@angular-devkit/build-angular (0.1200.0-next.4)

Commit + Description + Notes +
+ + + show warning during build when project requires IE 11 support +
+ + + only remove nomodule and defer attributes empty values + + + [Closes #20207]
+
+ +

@angular-devkit/core (12.0.0-next.4)

Commit + Description + Notes +
+ + + add handling for `defaultConfiguration` target definition property +

@angular/cli (12.0.0-next.4)

Commit + Description + Notes +
+ + + deprecate `--prod` command line argument +
+ + + add `defaultConfiguration` property to architect schema +
+ + + avoid exceptions for expected errors in architect commands +
+ + + add ng-packagr builder schema in IDE schema +

@ngtools/webpack (12.0.0-next.4)

Commit + Description + Notes +
+ + + drop support for string based lazy loading +

@schematics/angular (12.0.0-next.4)

Commit + Description + Notes +
+ + + add migration to update lazy loading string syntax to use dynamic imports +
+ + + add migration to remove `lazyModules` configuration option +
+ + + deprecate `legacyBrowsers` application and ng-new option +
+ + + production builds by default +
+ + + add `additionalProperties` to all schemas +
+ +--- + +# Breaking Changes + +

+ @angular-devkit/build-angular: remove deprecated `lazyModules` option (8d66912) +

+Server and Browser builder `lazyModules` option has been removed without replacement. + +

+ @ngtools/webpack: drop support for string based lazy loading (0dc7327) +

+With this change we drop support for string based lazy loading `./lazy.module#LazyModule` use dynamic imports instead. + +The following options which were used to support the above syntax were removed without replacement. + +- discoverLazyRoutes +- additionalLazyModules +- additionalLazyModuleResources +- contextElementDependencyConstructor + +--- + +# Special Thanks + +Alan Agius, Charles Lyding, Renovate Bot, Joey Perrott + + + + + +# v12.0.0-next.3 (2021-03-03) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (0.1200.0-next.3)

Commit + Description + Notes +
+ + + enable inlineCritical by default +
+ + + remove left-over `experimentalRollupPass` option +
+ + + inline critical font-face rules when using crittical css inlining +

@schematics/angular (12.0.0-next.3)

Commit + Description + Notes +
+ + + update ng new links +
+ +--- + +# Breaking Changes + +

+ @angular-devkit/build-angular: enable inlineCritical by default (aa3ea88) +

+Critical CSS inlining is now enabled by default. If you wish to turn this off set `inlineCritical` to `false`. + +See: https://angular.dev/reference/configs/workspace-config#optimization-configuration + +--- + +# Special Thanks + +Renovate Bot, Charles Lyding, Alan Agius, Keen Yee Liau, Douglas Parker, twerske + + + + + +# v12.0.0-next.2 (2021-02-24) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (0.1200.0-next.2)

Commit + Description + Notes +
+ + + only show index and service worker status once +
+ + + disable declaration and declarationMap + + + [Closes #20103]
+
+ +

@angular/cli (12.0.0-next.2)

Commit + Description + Notes +
+ + + remove npm 7 incompatibility notification +

@schematics/angular (12.0.0-next.2)

Commit + Description + Notes +
+ + + update new project dependencies version + + [Closes #20106]
+
+ +
+ + + augment `universal` schematics to import `platform-server` shims + + [Closes #40559]
+
+ +
+ + + add migration to remove emitDecoratorMetadata +
+ +--- + +--- + +# Special Thanks + +Renovate Bot, Charles Lyding, Alan Agius, Doug Parker, Joey Perrott, Jefiozie, George Kalpakas, Keen Yee Liau + + + + + +# v12.0.0-next.1 (2021-02-17) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (0.1200.0-next.1)

Commit + Description + Notes +
+ + + drop support for ng-packagr version 11 +
+ + + drop support for karma version 5.2 +

@angular-devkit/build-optimizer (0.1200.0-next.1)

Commit + Description + Notes +
+ + + support Webpack 5 +

@angular/cli (12.0.0-next.1)

Commit + Description + Notes +
+ + + support update migration packages with no entry points + + + [Closes #20032]
+
+ +
+ + + ensure odd number Node.js version message is a warning +
+ + + improve error logging when resolving update migrations +

@ngtools/webpack (12.0.0-next.1)

Commit + Description + Notes +
+ + + support Webpack 5 +
+ + + normalize paths when pruning AOT rebuild requests +

@schematics/angular (12.0.0-next.1)

Commit + Description + Notes +
+ + + add migration to use new zone.js entry-points +
+ + + use new zone.js entry-points +
+ +--- + +# Breaking Changes + +

+ @angular-devkit/build-angular: drop support for zone.js 0.10 (f309516) +

+Minimum supported `zone.js` version is `0.11.4` + +

+ @angular-devkit/build-angular: drop support for ng-packagr version 11 (44e75be) +

+Minimum supported `ng-packagr` version is `12.0.0-next` + +

+ @angular-devkit/build-angular: drop support for karma version 5.2 (fa5cf53) +

+Minimum supported `karma` version is `6.0.0` + +--- + +# Special Thanks + +Renovate Bot, Alan Agius, Charles Lyding, Keen Yee Liau, Aravind V Nair + + + + + +# v12.0.0-next.0 (2021-02-11) + +# Commits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@angular-devkit/build-angular (0.1200.0-next.0)

Commit + Description + Notes +
+ + + add `postcss-preset-env` with stage 3 features +
+ + + ensure i18n extraction sourcemaps are fully configured +
+ + + the root Tailwind configuration file is always picked +
+ + + fixed ignoring of karma plugins config + + + [Closes #19993]
+
+ +

@angular-devkit/core (12.0.0-next.0)

Commit + Description + Notes +
+ + + ensure job input values are processed in order +

@angular/cli (12.0.0-next.0)

Commit + Description + Notes +
+ + + update NPM 7 guidance +

@ngtools/webpack (12.0.0-next.0)

Commit + Description + Notes +
+ + + reduce overhead of Angular compiler rebuild requests +

@schematics/angular (12.0.0-next.0)

Commit + Description + Notes +
+ + + strict mode by default +
+ + + add migration to remove deprecated options from 'angular.json' +
+ + + only update removed v12 options in migration +
+ +--- + +# Breaking Changes + +

+ set minimum Node.js version to 12.13 (d1f6169) +

+Node.js version 10 will become EOL on 2021-04-30. +Angular CLI 12 will require Node.js 12.13+ or 14.15+. Node.js 12.13 and 14.15 are the first LTS releases for their respective majors. + +

+ @angular-devkit/build-angular: remove file-loader dependency (6732294) +

+The unsupported/undocumented, Webpack specific functionality to `import`/`require()` a non-module file has been removed. + +Before + +```js +import img from './images/asset.png'; +``` + +After + +```html + +``` + +--- + +# Special Thanks + +Renovate Bot, Charles Lyding, Alan Agius, Doug Parker, Bruno Baia, Amadou Sall, S. Iftekhar Hossain + +--- + +**Note: For release notes prior to this CHANGELOG see [release notes](https://github.com/angular/angular-cli/releases).** diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000000..501598396449 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,79 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering a safe and welcoming environment, we as +the Angular team pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity, gender expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Use welcoming and inclusive language +* Respect each other +* Provide and gracefully accept constructive criticism +* Show empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* The use of sexualized language or imagery +* Unwelcome sexual attention or advances +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Angular team are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Angular team have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, and to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies to all Angular communication channels - online or in person, +and it also applies when an individual is representing the project or its community in +public spaces. Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the Angular team at conduct@angular.io. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The Angular team +will maintain confidentiality with regard to the reporter of an incident. +Enforcement may result in an indefinite ban from all official Angular communication +channels, or other actions as deemed appropriate by the Angular team. + +Angular maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Appeal + +If you are banned you may contest the decision. To do so email conduct@angular.io with the subject line "Repeal Ban for {{your name here}}" and body with the responses to the following: + +* Why do you believe you did not violate the Code of Conduct? +* Were other factors involved in this situation the leadership team may have been unaware of? +* Why do you wish to be a part of the Angular community? + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42ab0020a6a6..0642e0b7ff65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,21 +12,26 @@ to follow: - [Coding Rules](#rules) - [Commit Message Guidelines](#commit) - [Signing the CLA](#cla) + - [Updating the Public API](#public-api) ## Code of Conduct Help us keep Angular open and inclusive. Please read and follow our [Code of Conduct][coc]. ## Got a Question or Problem? -Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [StackOverflow](https://stackoverflow.com/questions/tagged/angular-devkit) where the questions should be tagged with tag `angular-devkit`. +Please, do not open issues for the general support questions as we want to keep GitHub issues for +bug reports and feature requests. You've got much better chances of getting your question answered +on [StackOverflow](https://stackoverflow.com/questions/tagged/angular-devkit) where the questions +should be tagged with tag `angular-cli` or `angular-devkit`. StackOverflow is a much better place to ask questions since: -- there are thousands of people willing to help on StackOverflow -- questions and answers stay available for public viewing so your question / answer might help someone else +- There are thousands of people willing to help on StackOverflow. +- Questions and answers stay available for public viewing so your question / answer might help someone else. - StackOverflow's voting system assures that the best answers are prominently visible. -To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to StackOverflow. +To save your and our time we will be systematically closing all the issues that are requests for +general support and redirecting people to StackOverflow. If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter]. @@ -54,8 +59,9 @@ Before you submit an issue, please search the issue tracker, maybe an issue for We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. Having a reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like: -- version of Angular DevKit used +- version of Angular CLI used - `angular.json` configuration +- version of Angular DevKit used - 3rd-party libraries and their versions - and most importantly - a use-case that fails @@ -65,7 +71,7 @@ We will be insisting on a minimal reproduce scenario in order to save maintainer Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced. -You can file new issues by filling out our [new issue form](https://github.com/angular/angular-cli/issues/new). +You can file new issues by selecting from our [new issue templates](https://github.com/angular/angular-cli/issues/new/choose) and filling out the issue template. ### Submitting a Pull Request (PR) @@ -78,7 +84,7 @@ Before you submit your Pull Request (PR) consider the following guidelines: * Make your changes in a new git branch: ```shell - git checkout -b my-fix-branch master + git checkout -b my-fix-branch main ``` * Create your patch, **including appropriate test cases**. @@ -100,18 +106,25 @@ Before you submit your Pull Request (PR) consider the following guidelines: git push origin my-fix-branch ``` -* In GitHub, send a pull request to `devkit:master`. +* In GitHub, send a pull request to `angular/angular-cli:main`. * If we suggest changes then: * Make the required updates. * Re-run the Angular DevKit test suites to ensure tests are still passing. - * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): +* Once your PR is approved and you are done with any follow up changes: + * Rebase to the current main to pre-emptively address any merge conflicts. ```shell - git rebase master -i + git rebase upstream/main -i git push -f ``` + * Add the `action: merge` label and the correct +[target label](https://github.com/angular/angular/blob/main/docs/TRIAGE_AND_LABELS.md#pr-target) + (if PR author has the project collaborator status, or else the last reviewer + should do this). + * The current caretaker will merge the PR to the target branch(es) within 1-2 + business days. -That's it! Thank you for your contribution! +That's it! 🎉 Thank you for your contribution! #### After your pull request is merged @@ -124,10 +137,10 @@ from the main (upstream) repository: git push origin --delete my-fix-branch ``` -* Check out the master branch: +* Check out the main branch: ```shell - git checkout master -f + git checkout main -f ``` * Delete the local branch: @@ -136,10 +149,10 @@ from the main (upstream) repository: git branch -D my-fix-branch ``` -* Update your master with the latest upstream version: +* Update your local `main` with the latest upstream version: ```shell - git pull --ff upstream master + git pull --ff upstream main ``` ## Coding Rules @@ -179,52 +192,94 @@ If the commit reverts a previous commit, it should begin with `revert: `, follow ### Type Must be one of the following: -* **build**: Changes that affect the build system or external dependencies -* **ci**: Changes to our CI configuration files and scripts -* **docs**: Documentation only changes -* **feat**: A new feature -* **fix**: A bug fix -* **perf**: A code change that improves performance -* **refactor**: A code change that neither fixes a bug nor adds a feature -* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) -* **test**: Adding missing tests or correcting existing tests +* **build**: Changes to local repository build system and tooling +* **ci**: Changes to CI configuration and CI specific tooling [2] +* **docs**: Changes which exclusively affects documentation. +* **feat**: Creates a new feature [1] +* **fix**: Fixes a previously discovered failure/bug [1] +* **perf**: Improves performance without any change in functionality or API [1] +* **refactor**: Refactor without any change in functionality or API (includes style changes) +* **release**: A release point in the repository [2] +* **test**: Improvements or corrections made to the project's test suite + + +[1] This type MUST have a scope. See the next section for more information.
+[2] This type MUST NOT have a scope. It only applies to general scripts and tooling. ### Scope The scope should be the name of the npm package affected as perceived by the person reading changelog generated from the commit messages. The following is the list of supported scopes: +* **@angular/build** +* **@angular/cli** +* **@angular/create** +* **@angular/pwa** +* **@angular/ssr** +* **@angular-devkit/architect** +* **@angular-devkit/architect-cli** +* **@angular-devkit/build-angular** +* **@angular-devkit/build-webpack** * **@angular-devkit/core** -* **@angular-devkit/build-optimizer** * **@angular-devkit/schematics** * **@angular-devkit/schematics-cli** +* **@ngtools/webpack** * **@schematics/angular** -* **@schematics/schematics** -There are currently a few exceptions to the "use package name" rule: - -* **packaging**: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc. -* **changelog**: used for updating the release notes in CHANGELOG.md -* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`) ### Subject The subject contains succinct description of the change: * use the imperative, present tense: "change" not "changed" nor "changes" * don't capitalize first letter +* be concise and direct * no dot (.) at the end +### Examples +Examples of valid commit messages: + +* `fix(@angular/cli): prevent the flubber from grassing` +* `refactor(@schematics/angular): move all JSON classes together` + +Examples of invalid commit messages: +* `fix(@angular/cli): add a new XYZ command` + + This is a feature, not a fix. +* `ci(@angular/cli): fix publishing workflow` + + The `ci` type cannot have a scope. + ### Body Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior. ### Footer -The footer should contain any information about **Breaking Changes** and is also the place to -reference GitHub issues that this commit **Closes**. +The footer can contain information about breaking changes and deprecations. It is also the place to reference GitHub issues, Jira tickets, and other PRs that are related to this commit or that this commit will close. +For example: + +``` +BREAKING CHANGE: + + + + +Fixes # +``` + +or + +``` +DEPRECATED: + + + + +Closes # +``` -**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. +Breaking Change section should start with the phrase "BREAKING CHANGE: " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions. -A detailed explanation can be found in this [document][commit-message-format]. +Similarly, a Deprecation section should start with "DEPRECATED: " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path. ## Signing the CLA @@ -236,12 +291,25 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise [print, sign and one of scan+email, fax or mail the form][corporate-cla]. -[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md +[coc]: https://github.com/angular/code-of-conduct/blob/main/CODE_OF_CONDUCT.md [commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# -[corporate-cla]: http://code.google.com/legal/corporate-cla-v1.0.html -[dev-doc]: https://github.com/angular/angular-cli#development-hints-for-working-on-angular-cli -[GitHub]: https://github.com/angular/devkit +[corporate-cla]: https://code.google.com/legal/corporate-cla-v1.0.html +[dev-doc]: https://github.com/angular/angular-cli/blob/main/packages/angular/cli/README.md#development-hints-for-working-on-angular-cli +[GitHub]: https://github.com/angular/angular-cli [gitter]: https://gitter.im/angular/angular-cli -[individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html +[individual-cla]: https://code.google.com/legal/individual-cla-v1.0.html [js-style-guide]: https://google.github.io/styleguide/jsguide.html -[stackoverflow]: http://stackoverflow.com/questions/tagged/angular-devkit +[stackoverflow]: https://stackoverflow.com/questions/tagged/angular-devkit + +## Updating the Public API +Our Public API surface is tracked using golden files. + +You check all golden files by running: +```bash +pnpm public-api:check +``` + +If you modified the public API, the test will fail. To update the golden files you need to run: +```bash +pnpm public-api:update +``` diff --git a/LICENSE b/LICENSE index 8876c32c1969..48adc1eb1829 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/MODULE.bazel b/MODULE.bazel new file mode 100644 index 000000000000..f90ed9010d4c --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,5 @@ +# TODO(devversion): Investigate bzlmod and use it where possible. + +module( + name = "angular_cli", +) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock new file mode 100644 index 000000000000..3137c9f1d3fc --- /dev/null +++ b/MODULE.bazel.lock @@ -0,0 +1,110 @@ +{ + "lockFileVersion": 13, + "registryFileHashes": { + "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", + "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/source.json": "7e3a9adf473e9af076ae485ed649d5641ad50ec5c11718103f34de03170d94ad", + "https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef", + "https://bcr.bazel.build/modules/apple_support/1.5.0/source.json": "eb98a7627c0bc486b57f598ad8da50f6625d974c8f723e9ea71bd39f709c9862", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/source.json": "c9320aa53cd1c441d24bd6b716da087ad7e4ff0d9742a9884587596edfe53015", + "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", + "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/source.json": "082ed5f9837901fada8c68c2f3ddc958bb22b6d654f71dd73f3df30d45d4b749", + "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", + "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", + "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", + "https://bcr.bazel.build/modules/googletest/1.11.0/source.json": "c73d9ef4268c91bd0c1cd88f1f9dfa08e814b1dbe89b5f594a9f08ba0244d206", + "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", + "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", + "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", + "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", + "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", + "https://bcr.bazel.build/modules/platforms/0.0.9/source.json": "cd74d854bf16a9e002fb2ca7b1a421f4403cda29f824a765acd3a8c56f8d43e6", + "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://bcr.bazel.build/modules/protobuf/21.7/source.json": "bbe500720421e582ff2d18b0802464205138c06056f443184de39fbb8187b09b", + "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", + "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", + "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/source.json": "1f1ba6fea244b616de4a554a0f4983c91a9301640c8fe0dd1d410254115c8430", + "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", + "https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1", + "https://bcr.bazel.build/modules/rules_java/7.6.5/source.json": "a805b889531d1690e3c72a7a7e47a870d00323186a9904b36af83aa3d053ee8d", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/source.json": "a075731e1b46bc8425098512d038d416e966ab19684a10a34f4741295642fc35", + "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", + "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", + "https://bcr.bazel.build/modules/rules_license/0.0.7/source.json": "355cc5737a0f294e560d52b1b7a6492d4fff2caf0bef1a315df5a298fca2d34a", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/source.json": "c2557066e0c0342223ba592510ad3d812d4963b9024831f7f66fd0584dd8c66c", + "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/source.json": "d57902c052424dfda0e71646cb12668d39c4620ee0544294d9d941e7d12bc3a9", + "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", + "https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", + "https://bcr.bazel.build/modules/rules_python/0.22.1/source.json": "57226905e783bae7c37c2dd662be078728e48fa28ee4324a7eabcafb5a43d014", + "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", + "https://bcr.bazel.build/modules/stardoc/0.5.1/source.json": "a96f95e02123320aa015b956f29c00cb818fa891ef823d55148e1a362caacf29", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/source.json": "f1ef7d3f9e0e26d4b23d1c39b5f5de71f584dd7d1b4ef83d9bbba6ec7a6a6459", + "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d" + }, + "selectedYankedVersions": {}, + "moduleExtensions": { + "@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": { + "general": { + "bzlTransitiveDigest": "PjIds3feoYE8SGbbIq2SFTZy3zmxeO2tQevJZNDo7iY=", + "usagesDigest": "+hz7IHWN6A1oVJJWNDB6yZRG+RYhF76wAYItpAeIUIg=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "local_config_apple_cc_toolchains": { + "bzlFile": "@@apple_support~//crosstool:setup.bzl", + "ruleClassName": "_apple_cc_autoconf_toolchains", + "attributes": {} + }, + "local_config_apple_cc": { + "bzlFile": "@@apple_support~//crosstool:setup.bzl", + "ruleClassName": "_apple_cc_autoconf", + "attributes": {} + } + }, + "recordedRepoMappingEntries": [ + [ + "apple_support~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@platforms//host:extension.bzl%host_platform": { + "general": { + "bzlTransitiveDigest": "xelQcPZH8+tmuOHVjL9vDxMnnQNMlwj0SlvgoqBkm4U=", + "usagesDigest": "pCYpDQmqMbmiiPI1p2Kd3VLm5T48rRAht5WdW0X2GlA=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "host_platform": { + "bzlFile": "@@platforms//host:extension.bzl", + "ruleClassName": "host_platform_repo", + "attributes": {} + } + }, + "recordedRepoMappingEntries": [] + } + } + } +} diff --git a/README.md b/README.md index 69d7815d3e83..21a7d3f13398 100644 --- a/README.md +++ b/README.md @@ -10,81 +10,188 @@ Any changes to README.md directly will result in a failure on CI. --> -# Angular DevKit -### Development tools and libraries specialized for Angular +

Angular CLI - The CLI tool for Angular.

-This is the home of the DevKit and the Angular CLI code. You can find the Angular CLI specific README -[here](https://github.com/angular/angular-cli/blob/master/packages/angular/cli/README.md). +

+
+ Angular CLI logo +

+ The Angular CLI is a command-line interface tool that you use to initialize, develop, scaffold, +
and maintain Angular applications directly from a command shell.
+
+

+

+ angular.dev/tools/cli +
+

-[![CircleCI branch](https://img.shields.io/circleci/project/github/angular/angular-cli/master.svg?label=circleci)](https://circleci.com/gh/angular/angular-cli) [![Dependency Status](https://david-dm.org/angular/angular-cli.svg)](https://david-dm.org/angular/angular-cli) [![devDependency Status](https://david-dm.org/angular/angular-cli/dev-status.svg)](https://david-dm.org/angular/angular-cli?type=dev) +

+ Contributing Guidelines + · + Submit an Issue + · + Blog +
+
+

-[![License](https://img.shields.io/npm/l/@angular-angular-cli/core.svg)](https://github.com/angular/angular-cli/blob/master/LICENSE) +
-[![GitHub forks](https://img.shields.io/github/forks/angular/angular-cli.svg?style=social&label=Fork)](https://github.com/angular/angular-cli/fork) [![GitHub stars](https://img.shields.io/github/stars/angular/angular-cli.svg?style=social&label=Star)](https://github.com/angular/angular-cli) +## Documentation +Get started with Angular CLI, learn the fundamentals and explore advanced topics on our documentation website. +- [Getting started][quickstart] +- [CLI][cli] +- [Workspace and project file structure][filestructure] +- [Workspace configuration][workspaceconfig] +- [Schematics][schematics] ----- +## Development Setup -This is the home for all the tools and libraries built to assist developers with their Angular applications. -### Quick Links -[Gitter](https://gitter.im/angular/angular-cli) | [Contributing](https://github.com/angular/angular-cli/blob/master/CONTRIBUTING.md) | [Angular CLI](http://github.com/angular/angular-cli) | -|---|---|---| +### Prerequisites +- Install [Node.js] which includes [Node Package Manager][npm] -## The Goal of DevKit +### Setting Up a Project -Our goal is to provide a large set of libraries that can be used to manage, develop, deploy and -analyze your code. +Install the Angular CLI globally: -This is the extension of the Angular CLI Project. Once this set of tools is done, the Angular CLI -as it is today will become one of many interfaces available to perform those tasks. Everything -will also be available to third party tools and IDEs. +``` +npm install -g @angular/cli +``` +Create workspace: +``` +ng new [PROJECT NAME] +``` +Run the application: -## Tools +``` +cd [PROJECT NAME] +ng serve +``` -| Project | Package | Version | Links | -|---|---|---|---| -**Angular CLI** | [`@angular/cli`](https://npmjs.com/package/@angular/cli) | [![latest](https://img.shields.io/npm/v/%40angular%2Fcli/latest.svg)](https://npmjs.com/package/@angular/cli) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular/cli/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/cli-builds) -**Schematics CLI** | [`@angular-devkit/schematics-cli`](https://npmjs.com/package/@angular-devkit/schematics-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics-cli/latest.svg)](https://npmjs.com/package/@angular-devkit/schematics-cli) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-schematics-cli-builds) +Angular is cross-platform, fast, scalable, has incredible tooling, and is loved by millions. + +## Quickstart + +[Get started in 5 minutes][quickstart]. + +## Ecosystem + +

+ angular ecosystem logos +

+ +- [Angular Framework][adev] +- [Angular Material][angularmaterial] + +## Changelog + +[Learn about the latest improvements][changelog]. + +## Upgrading + +Check out our [upgrade guide](https://update.angular.dev/) to find out the best way to upgrade your project. + +## Contributing + +### Contributing Guidelines + +Read through our [contributing guidelines][contributing] to learn about our submission process, coding rules and more. + +### Want to Help? + +Want to report a bug, contribute some code, or improve documentation? Excellent! Read up on our guidelines for [contributing][contributing] and then check out one of our issues labeled as [help wanted](https://github.com/angular/angular-cli/labels/help%20wanted) or [good first issue](https://github.com/angular/angular-cli/labels/good%20first%20issue). + +### Code of Conduct + +Help us keep Angular open and inclusive. Please read and follow our [Code of Conduct][codeofconduct]. + +### Developer Guide +Read through our [developer guide][developer] to learn about how to build and test the Angular CLI locally. +## Community + +Join the conversation and help the community. + +- [X (formerly Twitter)][twitter] +- [Discord][discord] +- [Gitter][gitter] +- [YouTube][youtube] +- [StackOverflow][stackoverflow] +- Find a Local [Meetup][meetup] + ## Packages -This is a monorepo which contains many packages: + +This is a monorepo which contains many tools and packages: + +### Tools | Project | Package | Version | Links | |---|---|---|---| -**Architect** | [`@angular-devkit/architect`](https://npmjs.com/package/@angular-devkit/architect) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect/latest.svg)](https://npmjs.com/package/@angular-devkit/architect) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/architect/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-architect-builds) +**Angular Build System** | [`@angular/build`](https://npmjs.com/package/@angular/build) | [![latest](https://img.shields.io/npm/v/%40angular%2Fbuild/latest.svg)](https://npmjs.com/package/@angular/build) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular/build/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-build-builds) +**Angular CLI** | [`@angular/cli`](https://npmjs.com/package/@angular/cli) | [![latest](https://img.shields.io/npm/v/%40angular%2Fcli/latest.svg)](https://npmjs.com/package/@angular/cli) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular/cli/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/cli-builds) **Architect CLI** | [`@angular-devkit/architect-cli`](https://npmjs.com/package/@angular-devkit/architect-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect-cli/latest.svg)](https://npmjs.com/package/@angular-devkit/architect-cli) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-architect-cli-builds) -**Build Angular** | [`@angular-devkit/build-angular`](https://npmjs.com/package/@angular-devkit/build-angular) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-angular/latest.svg)](https://npmjs.com/package/@angular-devkit/build-angular) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_angular/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-angular-builds) -**Build NgPackagr** | [`@angular-devkit/build-ng-packagr`](https://npmjs.com/package/@angular-devkit/build-ng-packagr) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-ng-packagr/latest.svg)](https://npmjs.com/package/@angular-devkit/build-ng-packagr) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_ng_packagr/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-ng-packagr-builds) -**Build Optimizer** | [`@angular-devkit/build-optimizer`](https://npmjs.com/package/@angular-devkit/build-optimizer) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-optimizer/latest.svg)](https://npmjs.com/package/@angular-devkit/build-optimizer) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_optimizer/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-optimizer-builds) -**Build Webpack** | [`@angular-devkit/build-webpack`](https://npmjs.com/package/@angular-devkit/build-webpack) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-webpack/latest.svg)](https://npmjs.com/package/@angular-devkit/build-webpack) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_webpack/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-webpack-builds) -**Core** | [`@angular-devkit/core`](https://npmjs.com/package/@angular-devkit/core) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fcore/latest.svg)](https://npmjs.com/package/@angular-devkit/core) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/core/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-core-builds) -**Schematics** | [`@angular-devkit/schematics`](https://npmjs.com/package/@angular-devkit/schematics) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics/latest.svg)](https://npmjs.com/package/@angular-devkit/schematics) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/schematics/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-schematics-builds) +**Schematics CLI** | [`@angular-devkit/schematics-cli`](https://npmjs.com/package/@angular-devkit/schematics-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics-cli/latest.svg)](https://npmjs.com/package/@angular-devkit/schematics-cli) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-schematics-cli-builds) + + +### Packages -#### Schematics | Project | Package | Version | Links | |---|---|---|---| -**Angular PWA Schematics** | [`@angular/pwa`](https://npmjs.com/package/@angular/pwa) | [![latest](https://img.shields.io/npm/v/%40angular%2Fpwa/latest.svg)](https://npmjs.com/package/@angular/pwa) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-pwa-builds) -**Angular Schematics** | [`@schematics/angular`](https://npmjs.com/package/@schematics/angular) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fangular/latest.svg)](https://npmjs.com/package/@schematics/angular) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/schematics-angular-builds) -**Package JSON Update Schematics** | [`@schematics/package-update`](https://npmjs.com/package/@schematics/package-update) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fpackage-update/latest.svg)](https://npmjs.com/package/@schematics/package-update) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/schematics-package-update-builds) -**Schematics Schematics** | [`@schematics/schematics`](https://npmjs.com/package/@schematics/schematics) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fschematics/latest.svg)](https://npmjs.com/package/@schematics/schematics) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/schematics-schematics-builds) -**Package Update Schematics** | [`@schematics/update`](https://npmjs.com/package/@schematics/update) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fupdate/latest.svg)](https://npmjs.com/package/@schematics/update) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/schematics-update-builds) +**Angular SSR** | [`@angular/ssr`](https://npmjs.com/package/@angular/ssr) | [![latest](https://img.shields.io/npm/v/%40angular%2Fssr/latest.svg)](https://npmjs.com/package/@angular/ssr) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular/ssr/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-ssr-builds) +**Architect** | [`@angular-devkit/architect`](https://npmjs.com/package/@angular-devkit/architect) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect/latest.svg)](https://npmjs.com/package/@angular-devkit/architect) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/architect/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-architect-builds) +**Build Angular** | [`@angular-devkit/build-angular`](https://npmjs.com/package/@angular-devkit/build-angular) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-angular/latest.svg)](https://npmjs.com/package/@angular-devkit/build-angular) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/build_angular/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-angular-builds) +**Build Webpack** | [`@angular-devkit/build-webpack`](https://npmjs.com/package/@angular-devkit/build-webpack) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-webpack/latest.svg)](https://npmjs.com/package/@angular-devkit/build-webpack) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/build_webpack/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-webpack-builds) +**Core** | [`@angular-devkit/core`](https://npmjs.com/package/@angular-devkit/core) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fcore/latest.svg)](https://npmjs.com/package/@angular-devkit/core) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/core/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-core-builds) +**Schematics** | [`@angular-devkit/schematics`](https://npmjs.com/package/@angular-devkit/schematics) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics/latest.svg)](https://npmjs.com/package/@angular-devkit/schematics) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/schematics/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-schematics-builds) #### Misc | Project | Package | Version | Links | |---|---|---|---| +**Angular Create** | [`@angular/create`](https://npmjs.com/package/@angular/create) | [![latest](https://img.shields.io/npm/v/%40angular%2Fcreate/latest.svg)](https://npmjs.com/package/@angular/create) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular/create/README.md) **Webpack Angular Plugin** | [`@ngtools/webpack`](https://npmjs.com/package/@ngtools/webpack) | [![latest](https://img.shields.io/npm/v/%40ngtools%2Fwebpack/latest.svg)](https://npmjs.com/package/@ngtools/webpack) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/ngtools-webpack-builds) +#### Schematics + +| Project | Package | Version | Links | +|---|---|---|---| +**Angular PWA Schematics** | [`@angular/pwa`](https://npmjs.com/package/@angular/pwa) | [![latest](https://img.shields.io/npm/v/%40angular%2Fpwa/latest.svg)](https://npmjs.com/package/@angular/pwa) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-pwa-builds) +**Angular Schematics** | [`@schematics/angular`](https://npmjs.com/package/@schematics/angular) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fangular/latest.svg)](https://npmjs.com/package/@schematics/angular) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/schematics-angular-builds) + + +**Love Angular CLI? Give our repo a star :star: :arrow_up:.** + +[contributing]: CONTRIBUTING.md +[developer]: docs/DEVELOPER.md +[quickstart]: https://angular.dev/tutorials/learn-angular +[changelog]: CHANGELOG.md +[documentation]: https://angular.dev/overview +[angularmaterial]: https://material.angular.io/ +[cli]: https://angular.dev/tools/cli +[adev]: https://angular.dev/ +[workspaceconfig]: https://angular.dev/reference/configs/workspace-config +[schematics]: https://angular.dev/tools/cli/schematics +[filestructure]: https://angular.dev/reference/configs/file-structure +[node.js]: https://nodejs.org/ +[npm]: https://www.npmjs.com/get-npm +[codeofconduct]: https://github.com/angular/angular/blob/main/CODE_OF_CONDUCT.md +[twitter]: https://www.x.com/angular +[discord]: https://discord.gg/angular +[gitter]: https://gitter.im/angular/angular-cli +[stackoverflow]: https://stackoverflow.com/questions/tagged/angular-cli +[youtube]: https://youtube.com/angular +[meetup]: https://www.meetup.com/find/?keywords=angular diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..0361308689ab --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +Angular is part of Google [Open Source Software Vulnerability Reward Program](https://bughunters.google.com/about/rules/6521337925468160/google-open-source-software-vulnerability-reward-program-rules). For vulnerabilities in Angular, please submit your report [here](https://bughunters.google.com/report). + +For more information, check out [Angular's security policy](https://angular.dev/best-practices/security). diff --git a/WORKSPACE b/WORKSPACE index 794f4f95a035..59c97586f6bd 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1,52 +1,289 @@ -workspace(name = "angular_devkit") +workspace(name = "angular_cli") + +DEFAULT_NODE_VERSION = "20.11.1" + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") + +http_archive( + name = "bazel_skylib", + sha256 = "bc283cdfcd526a52c3201279cda4bc298652efa898b10b4db0837dc51652756f", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.7.1/bazel-skylib-1.7.1.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.7.1/bazel-skylib-1.7.1.tar.gz", + ], +) + +http_archive( + name = "io_bazel_rules_webtesting", + sha256 = "e9abb7658b6a129740c0b3ef6f5a2370864e102a5ba5ffca2cea565829ed825a", + urls = ["https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.5/rules_webtesting.tar.gz"], +) http_archive( name = "build_bazel_rules_nodejs", - url = "https://github.com/bazelbuild/rules_nodejs/archive/0.4.1.zip", - strip_prefix = "rules_nodejs-0.4.1", - sha256 = "e9bc013417272b17f302dc169ad597f05561bb277451f010043f4da493417607", + sha256 = "5dd1e5dea1322174c57d3ca7b899da381d516220793d0adef3ba03b9d23baa8e", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.3/rules_nodejs-5.8.3.tar.gz"], +) + +load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies") + +build_bazel_rules_nodejs_dependencies() + +http_archive( + name = "aspect_rules_js", + sha256 = "83e5af4d17385d1c3268c31ae217dbfc8525aa7bcf52508dc6864baffc8b9501", + strip_prefix = "rules_js-2.3.7", + url = "https://github.com/aspect-build/rules_js/releases/download/v2.3.7/rules_js-v2.3.7.tar.gz", +) + +load("@aspect_rules_js//js:repositories.bzl", "rules_js_dependencies") + +rules_js_dependencies() + +http_archive( + name = "rules_pkg", + sha256 = "8c20f74bca25d2d442b327ae26768c02cf3c99e93fad0381f32be9aab1967675", + urls = ["https://github.com/bazelbuild/rules_pkg/releases/download/0.8.1/rules_pkg-0.8.1.tar.gz"], +) + +load("@bazel_tools//tools/sh:sh_configure.bzl", "sh_configure") + +sh_configure() + +load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") + +bazel_skylib_workspace() + +load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies") + +rules_pkg_dependencies() + +# Setup the Node.js toolchain +load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains") + +NODE_20_REPO = { + "20.11.1-darwin_arm64": ("node-v20.11.1-darwin-arm64.tar.gz", "node-v20.11.1-darwin-arm64", "e0065c61f340e85106a99c4b54746c5cee09d59b08c5712f67f99e92aa44995d"), + "20.11.1-darwin_amd64": ("node-v20.11.1-darwin-x64.tar.gz", "node-v20.11.1-darwin-x64", "c52e7fb0709dbe63a4cbe08ac8af3479188692937a7bd8e776e0eedfa33bb848"), + "20.11.1-linux_arm64": ("node-v20.11.1-linux-arm64.tar.xz", "node-v20.11.1-linux-arm64", "c957f29eb4e341903520caf362534f0acd1db7be79c502ae8e283994eed07fe1"), + "20.11.1-linux_ppc64le": ("node-v20.11.1-linux-ppc64le.tar.xz", "node-v20.11.1-linux-ppc64le", "51343cacf5cdf5c4b5e93e919d19dd373d6ef43d5f2c666eae299f26e31d08b5"), + "20.11.1-linux_s390x": ("node-v20.11.1-linux-s390x.tar.xz", "node-v20.11.1-linux-s390x", "b32616b705cd0ddbb230b95c693e3d7a37becc2ced9bcadea8dc824cceed6be0"), + "20.11.1-linux_amd64": ("node-v20.11.1-linux-x64.tar.xz", "node-v20.11.1-linux-x64", "d8dab549b09672b03356aa2257699f3de3b58c96e74eb26a8b495fbdc9cf6fbe"), + "20.11.1-windows_amd64": ("node-v20.11.1-win-x64.zip", "node-v20.11.1-win-x64", "bc032628d77d206ffa7f133518a6225a9c5d6d9210ead30d67e294ff37044bda"), +} + +# Set the default nodejs toolchain to the latest supported major version +nodejs_register_toolchains( + name = "nodejs", + # The below can be removed once @rules_nodejs/nodejs is updated to latest which contains https://github.com/bazelbuild/rules_nodejs/pull/3701 + node_repositories = NODE_20_REPO, + node_version = DEFAULT_NODE_VERSION, +) + +nodejs_register_toolchains( + name = "node20", + # The below can be removed once @rules_nodejs/nodejs is updated to latest which contains https://github.com/bazelbuild/rules_nodejs/pull/3701 + node_repositories = NODE_20_REPO, + node_version = "20.11.1", +) + +nodejs_register_toolchains( + name = "node22", + # The below can be removed once @rules_nodejs/nodejs is updated to latest which contains https://github.com/bazelbuild/rules_nodejs/pull/3701 + node_repositories = { + "22.11.0-darwin_arm64": ("node-v22.11.0-darwin-arm64.tar.gz", "node-v22.11.0-darwin-arm64", "2e89afe6f4e3aa6c7e21c560d8a0453d84807e97850bbb819b998531a22bdfde"), + "22.11.0-darwin_amd64": ("node-v22.11.0-darwin-x64.tar.gz", "node-v22.11.0-darwin-x64", "668d30b9512137b5f5baeef6c1bb4c46efff9a761ba990a034fb6b28b9da2465"), + "22.11.0-linux_arm64": ("node-v22.11.0-linux-arm64.tar.xz", "node-v22.11.0-linux-arm64", "6031d04b98f59ff0f7cb98566f65b115ecd893d3b7870821171708cdbaf7ae6e"), + "22.11.0-linux_ppc64le": ("node-v22.11.0-linux-ppc64le.tar.xz", "node-v22.11.0-linux-ppc64le", "d1d49d7d611b104b6d616e18ac439479d8296aa20e3741432de0e85f4735a81e"), + "22.11.0-linux_s390x": ("node-v22.11.0-linux-s390x.tar.xz", "node-v22.11.0-linux-s390x", "f474ed77d6b13d66d07589aee1c2b9175be4c1b165483e608ac1674643064a99"), + "22.11.0-linux_amd64": ("node-v22.11.0-linux-x64.tar.xz", "node-v22.11.0-linux-x64", "83bf07dd343002a26211cf1fcd46a9d9534219aad42ee02847816940bf610a72"), + "22.11.0-windows_amd64": ("node-v22.11.0-win-x64.zip", "node-v22.11.0-win-x64", "905373a059aecaf7f48c1ce10ffbd5334457ca00f678747f19db5ea7d256c236"), + }, + node_version = "22.11.0", +) + +load("@aspect_rules_js//js:toolchains.bzl", "rules_js_register_toolchains") + +rules_js_register_toolchains( + node_repositories = NODE_20_REPO, + node_version = DEFAULT_NODE_VERSION, +) + +http_archive( + name = "aspect_bazel_lib", + sha256 = "2be8a5df0b20b0ed37604b050da01dbf7ad45ad44768c0d478b64779b9f58412", + strip_prefix = "bazel-lib-2.15.3", + url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.15.3/bazel-lib-v2.15.3.tar.gz", ) -load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories") +load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "aspect_bazel_lib_register_toolchains") -check_bazel_version("0.9.0") -node_repositories(package_json = ["//:package.json"]) +aspect_bazel_lib_dependencies() -local_repository( - name = "rxjs", - path = "node_modules/rxjs/src", +aspect_bazel_lib_register_toolchains() + +load("@build_bazel_rules_nodejs//toolchains/esbuild:esbuild_repositories.bzl", "esbuild_repositories") + +esbuild_repositories( + npm_repository = "npm", +) + +load("@aspect_rules_js//npm:repositories.bzl", "npm_translate_lock") + +npm_translate_lock( + name = "npm", + custom_postinstalls = { + # TODO: Standardize browser management for `rules_js` + "webdriver-manager": "node ./bin/webdriver-manager update --standalone false --gecko false --versions.chrome 106.0.5249.21", + }, + data = [ + "//:package.json", + "//:pnpm-workspace.yaml", + "//modules/testing/builder:package.json", + "//packages/angular/build:package.json", + "//packages/angular/cli:package.json", + "//packages/angular/pwa:package.json", + "//packages/angular/ssr:package.json", + "//packages/angular_devkit/architect:package.json", + "//packages/angular_devkit/architect_cli:package.json", + "//packages/angular_devkit/build_angular:package.json", + "//packages/angular_devkit/build_webpack:package.json", + "//packages/angular_devkit/core:package.json", + "//packages/angular_devkit/schematics:package.json", + "//packages/angular_devkit/schematics_cli:package.json", + "//packages/ngtools/webpack:package.json", + "//packages/schematics/angular:package.json", + "//tests:package.json", + "//tools/baseline_browserslist:package.json", + ], + lifecycle_hooks_envs = { + # TODO: Standardize browser management for `rules_js` + "puppeteer": ["PUPPETEER_DOWNLOAD_PATH=./downloads"], + }, + lifecycle_hooks_execution_requirements = { + # Needed for downloading chromedriver. + # Also `update-config` of webdriver manager would store an absolute path; + # which would then break execution. + "webdriver-manager": ["local"], + }, + npmrc = "//:.npmrc", + patches = { + # Note: Patches not needed as the existing patches are only + # for `rules_nodejs` dependencies :) + }, + pnpm_lock = "//:pnpm-lock.yaml", + public_hoist_packages = { + # TODO: Remove when https://github.com/verdaccio/verdaccio/commit/bf0e09a509e8e0a74167b0307d129202bc3f40d2 is available. + "@verdaccio/config": [""], + }, + verify_node_modules_ignored = "//:.bazelignore", ) -# Pick up the fix for source-map typings -RULES_TYPESCRIPT_VERSION = "00f8fd5467f2b12ac2fbb8d74ea81d2dd5636d31" +load("@npm//:repositories.bzl", "npm_repositories") + +npm_repositories() + http_archive( - name = "build_bazel_rules_typescript", - url = "https://github.com/bazelbuild/rules_typescript/archive/%s.zip" % RULES_TYPESCRIPT_VERSION, - strip_prefix = "rules_typescript-%s" % RULES_TYPESCRIPT_VERSION, - sha256 = "3606b97a4859cc3f73b47888618b14290cf4b93c411b1bedd821e8bb39b3442b", + name = "aspect_rules_ts", + sha256 = "56858e1e4380948e2d5aca5ab2e96fc5ed788652a4a3b7036e8e4b6f019e63bd", + strip_prefix = "rules_ts-3.5.3", + url = "https://github.com/aspect-build/rules_ts/releases/download/v3.5.3/rules_ts-v3.5.3.tar.gz", ) -load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace") +load("@aspect_rules_ts//ts:repositories.bzl", "rules_ts_dependencies") -ts_setup_workspace() +rules_ts_dependencies( + # ts_version_from = "//:package.json", + # Obtained by: curl --silent https://registry.npmjs.org/typescript/5.7.2 | jq -r '.dist.integrity' + ts_integrity = "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + ts_version = "5.7.2", +) + +http_file( + name = "tsc_worker", + sha256 = "5a5c46846ecda83e05b9da26f1672ad51c59bce08fed88419850d0e29c993b30", + urls = ["https://raw.githubusercontent.com/devversion/rules_angular/4b7532ba2b29078d005899cd15b415593d03cceb/dist/worker.mjs"], +) + +http_archive( + name = "aspect_rules_jasmine", + sha256 = "0d2f9c977842685895020cac721d8cc4f1b37aae15af46128cf619741dc61529", + strip_prefix = "rules_jasmine-2.0.0", + url = "https://github.com/aspect-build/rules_jasmine/releases/download/v2.0.0/rules_jasmine-v2.0.0.tar.gz", +) + +load("@aspect_rules_jasmine//jasmine:dependencies.bzl", "rules_jasmine_dependencies") + +rules_jasmine_dependencies() + +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") -# We get tools like Buildifier from here git_repository( - name = "com_github_bazelbuild_buildtools", - remote = "https://github.com/bazelbuild/buildtools.git", - commit = "b3b620e8bcff18ed3378cd3f35ebeb7016d71f71", + name = "devinfra", + commit = "a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7", + remote = "https://github.com/angular/dev-infra.git", +) + +load("@devinfra//bazel:setup_dependencies_1.bzl", "setup_dependencies_1") + +setup_dependencies_1() + +load("@devinfra//bazel:setup_dependencies_2.bzl", "setup_dependencies_2") + +setup_dependencies_2() + +load("@devinfra//bazel/browsers:browser_repositories.bzl", "browser_repositories") + +browser_repositories() + +register_toolchains( + "@devinfra//bazel/git-toolchain:git_linux_toolchain", + "@devinfra//bazel/git-toolchain:git_macos_x86_toolchain", + "@devinfra//bazel/git-toolchain:git_macos_arm64_toolchain", + "@devinfra//bazel/git-toolchain:git_windows_toolchain", ) -# The Go toolchain is used for Buildifier and some TypeScript tooling. http_archive( - name = "io_bazel_rules_go", - url = "https://github.com/bazelbuild/rules_go/releases/download/0.7.1/rules_go-0.7.1.tar.gz", - sha256 = "341d5eacef704415386974bc82a1783a8b7ffbff2ab6ba02375e1ca20d9b031c", + name = "aspect_rules_esbuild", + sha256 = "530adfeae30bbbd097e8af845a44a04b641b680c5703b3bf885cbd384ffec779", + strip_prefix = "rules_esbuild-0.22.1", + url = "https://github.com/aspect-build/rules_esbuild/releases/download/v0.22.1/rules_esbuild-v0.22.1.tar.gz", ) -load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains") +load("@aspect_rules_esbuild//esbuild:dependencies.bzl", "rules_esbuild_dependencies") -go_rules_dependencies() +rules_esbuild_dependencies() -go_register_toolchains() +load("@aspect_rules_esbuild//esbuild:repositories.bzl", "LATEST_ESBUILD_VERSION", "esbuild_register_toolchains") + +esbuild_register_toolchains( + name = "esbuild", + esbuild_version = LATEST_ESBUILD_VERSION, +) + +git_repository( + name = "rules_angular", + commit = "3ba9d6790055543de2125e11967d3b5a7c7b314d", + remote = "https://github.com/devversion/rules_angular.git", +) +load("@rules_angular//setup:step_1.bzl", "rules_angular_step1") + +rules_angular_step1() + +load("@rules_angular//setup:step_2.bzl", "rules_angular_step2") + +rules_angular_step2() + +load("@rules_angular//setup:step_3.bzl", "rules_angular_step3") + +rules_angular_step3( + angular_compiler_cli = "//:node_modules/@angular/compiler-cli", + typescript = "//:node_modules/typescript", +) + +http_archive( + name = "aspect_rules_rollup", + sha256 = "0b8ac7d97cd660eb9a275600227e9c4268f5904cba962939d1a6ce9a0a059d2e", + strip_prefix = "rules_rollup-2.0.1", + url = "https://github.com/aspect-build/rules_rollup/releases/download/v2.0.1/rules_rollup-v2.0.1.tar.gz", +) diff --git a/bin/architect b/bin/architect deleted file mode 100755 index 7885feefe26a..000000000000 --- a/bin/architect +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node -/** - * @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 strict'; - - -require('../lib/bootstrap-local'); -const packages = require('../lib/packages').packages; -require(packages['@angular-devkit/architect-cli'].bin['architect']); diff --git a/bin/build-optimizer b/bin/build-optimizer deleted file mode 100755 index 6f540387043c..000000000000 --- a/bin/build-optimizer +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node -/** - * @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 strict'; - - -require('../lib/bootstrap-local'); -const packages = require('../lib/packages').packages; -require(packages['@angular-devkit/build-optimizer'].bin['build-optimizer']); diff --git a/bin/devkit-admin b/bin/devkit-admin deleted file mode 100755 index be8719fd60ca..000000000000 --- a/bin/devkit-admin +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env node -/** - * @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 strict'; - -/** - * This file is useful for not having to load bootstrap-local in various javascript. - * Simply use package.json to have npm scripts that use this script as well, or use - * this script directly. - */ -require('../lib/bootstrap-local'); - - -const minimist = require('minimist'); -const path = require('path'); - -const args = minimist(process.argv.slice(2), { - boolean: ['verbose'] -}); -const scriptName = args._.shift(); -const scriptPath = path.join('../scripts', scriptName); - -process.chdir(path.join(__dirname, '..')); - - -// This might get awkward, so we fallback to console if there was an error. -let logger = null; -try { - logger = new (require('@angular-devkit/core').logging.IndentLogger)('root'); - const { bold, gray, red, yellow, white } = require('@angular-devkit/core').terminal; - const filter = require('rxjs/operators').filter; - - logger - .pipe(filter(entry => (entry.level !== 'debug' || args.verbose))) - .subscribe(entry => { - let color = gray; - let output = process.stdout; - switch (entry.level) { - case 'info': color = white; break; - case 'warn': color = yellow; break; - case 'error': color = red; output = process.stderr; break; - case 'fatal': color = x => bold(red(x)); output = process.stderr; break; - } - - output.write(color(entry.message) + '\n'); - }); -} catch (e) { - console.error(`Reverting to manual console logging.\nReason: ${e.message}.`); - logger = { - debug: console.log.bind(console), - info: console.log.bind(console), - warn: console.warn.bind(console), - error: console.error.bind(console), - fatal: x => { console.error(x); process.exit(100); }, - createChild: () => logger, - }; -} - - -try { - Promise.resolve() - .then(() => require(scriptPath).default(args, logger)) - .then(exitCode => process.exit(exitCode || 0)) - .catch(err => { - logger.fatal(err.stack); - process.exit(99); - }); -} catch (err) { - logger.fatal(err.stack); - process.exit(99); -} - diff --git a/bin/ng b/bin/ng deleted file mode 100755 index d43c5512e47b..000000000000 --- a/bin/ng +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node -/** - * @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 strict'; - - -require('../lib/bootstrap-local'); -const packages = require('../lib/packages').packages; -require(packages['@angular/cli'].bin['ng']); diff --git a/bin/purify b/bin/purify deleted file mode 100755 index 810b4d6820b9..000000000000 --- a/bin/purify +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node -/** - * @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 strict'; - - -require('../lib/bootstrap-local'); -const packages = require('../lib/packages').packages; -require(packages['@angular-devkit/build-optimizer'].bin['purify']); diff --git a/bin/schematics b/bin/schematics deleted file mode 100755 index 748599df46c9..000000000000 --- a/bin/schematics +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node -/** - * @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 strict'; - - -require('../lib/bootstrap-local'); -const packages = require('../lib/packages').packages; -require(packages['@angular-devkit/schematics-cli'].bin['schematics']); diff --git a/constants.bzl b/constants.bzl new file mode 100644 index 000000000000..4c42bcb8dbb9 --- /dev/null +++ b/constants.bzl @@ -0,0 +1,32 @@ +# Engine versions to stamp in a release package.json +RELEASE_ENGINES_NODE = "^20.11.1 || >=22.11.0" +RELEASE_ENGINES_NPM = "^6.11.0 || ^7.5.6 || >=8.0.0" +RELEASE_ENGINES_YARN = ">= 1.13.0" + +NG_PACKAGR_VERSION = "^20.0.0-next.0" +ANGULAR_FW_VERSION = "^20.0.0-next.0" +ANGULAR_FW_PEER_DEP = "^20.0.0 || ^20.0.0-next.0" +NG_PACKAGR_PEER_DEP = "^20.0.0 || ^20.0.0-next.0" + +# Baseline widely-available date in `YYYY-MM-DD` format which defines Angular's +# browser support. This date serves as the source of truth for the Angular CLI's +# default browser set used to determine what downleveling is necessary. +# +# See: https://web.dev/baseline +BASELINE_DATE = "2025-04-30" + +SNAPSHOT_REPOS = { + "@angular/cli": "angular/cli-builds", + "@angular/pwa": "angular/angular-pwa-builds", + "@angular/build": "angular/angular-build-builds", + "@angular/ssr": "angular/angular-ssr-builds", + "@angular-devkit/architect": "angular/angular-devkit-architect-builds", + "@angular-devkit/architect-cli": "angular/angular-devkit-architect-cli-builds", + "@angular-devkit/build-angular": "angular/angular-devkit-build-angular-builds", + "@angular-devkit/build-webpack": "angular/angular-devkit-build-webpack-builds", + "@angular-devkit/core": "angular/angular-devkit-core-builds", + "@angular-devkit/schematics": "angular/angular-devkit-schematics-builds", + "@angular-devkit/schematics-cli": "angular/angular-devkit-schematics-cli-builds", + "@ngtools/webpack": "angular/ngtools-webpack-builds", + "@schematics/angular": "angular/schematics-angular-builds", +} diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md new file mode 100644 index 000000000000..640e83447ae8 --- /dev/null +++ b/docs/DEVELOPER.md @@ -0,0 +1,189 @@ +# Building and Testing Angular CLI + +## Installation + +To get started locally, follow these instructions: + +1. If you haven't done it already, [make a fork of this repo](https://github.com/angular/angular-cli/fork). +2. If you are on Windows, see [the extra steps needed for contributing on Windows](#windows) +3. Clone to your local computer using `git`. +4. Make sure that you have Node `v20.18.1` or higher installed. See instructions [here](https://nodejs.org/en/download/). +5. Install `pnpm`. + - You can install pnpm by running `npm i -g pnpm@9`. + - See detailed instructions [here](https://pnpm.io/installation). +6. Run `pnpm install` from the root of your clone of this project to install dependencies. + +## Building and Installing the CLI + +To make a local build: + +```shell +pnpm build --local +``` + +This generates a number of tarballs in the `dist/` directory. To actually use +the locally built tools, switch to another repository reproducing the specific +issue you want to fix (or just generate a local repo with `ng new`). Then +install the locally built packages: + +```shell +cd "${EXAMPLE_ANGULAR_PROJECT_REPO}" +npm install -D ${CLI_REPO}/dist/*.tgz +``` + +Builds of this example project will use tooling created from the previous local +build and include any local changes. When using the CLI, it will automatically +check for a local install and use that if present. This means you can just run: + +```shell +npm install -g @angular/cli +``` + +to get a global install of the latest CLI release. Then running any `ng` command +in the example project will automatically find and use the local build of the +CLI. + +Note: If you are testing `ng update`, be aware that installing all the tarballs +will also update the framework (`@angular/core`) to the latest version. In this +case, simply install the CLI alone with +`npm install -D ${CLI_REPO}/dist/_angular_cli.tgz`, that way the rest of the +project remains to be upgraded with `ng update`. + +## Debugging + +To debug an invocation of the CLI, [build and install the CLI for an example +project](#building-and-installing-the-cli), then run the desired `ng` command +as: + +```shell +node --inspect-brk node_modules/.bin/ng ... +``` + +This will trigger a breakpoint as the CLI starts up. You can connect to this +using the supported mechanisms for your IDE, but the simplest option is to open +Chrome to [chrome://inspect](chrome://inspect) and then click on the `inspect` +link for the `node_modules/.bin/ng` Node target. + +Unfortunately, the CLI dynamically `require()`'s other files mid-execution, so +the debugger is not aware of all the source code files before hand. As a result, +it is tough to put breakpoints on files before the CLI loads them. The easiest +workaround is to use the `debugger;` statement to stop execution in the file you +are interested in, and then you should be able to step around and set breakpoints +as expected. + +## Testing + +There are two different test suites which can be run locally: + +### Unit tests + +- Run all tests: `pnpm bazel test //packages/...` +- Run a subset of the tests, use the full Bazel target example: `pnpm bazel test //packages/schematics/angular:angular_test` +- For a complete list of test targets use the following Bazel query: `pnpm bazel query "tests(//packages/...)"` + +When debugging a specific test, change `describe()` or `it()` to `fdescribe()` +and `fit()` to focus execution to just that one test. This will keep the output clean and speed up execution by not running irrelevant tests. + +You can find more info about debugging [tests with Bazel in the docs.](https://github.com/angular/angular-cli/blob/main/docs/process/bazel.md#debugging-jasmine_node_test) + +### End to end tests + +- For a complete list of test targets use the following Bazel query: `pnpm bazel query "tests(//tests/...)"` +- Run a subset of the tests: `pnpm bazel test //tests/legacy-cli:e2e_node22 --config=e2e --test_filter="tests/i18n/ivy-localize-*"` +- Use `bazel run` to debug failing tests debugging: `pnpm bazel run //tests/legacy-cli:e2e_node22 --config=e2e --test_arg="--glob=tests/basic/aot.ts"` +- Provide additional `e2e_runner` options using `--test_arg`: `--test_arg="--package-manager=yarn"` + +When running the debug commands, Node will stop and wait for a debugger to attach. +You can attach your IDE to the debugger to stop on breakpoints and step through the code. Also, see [IDE Specific Usage](#ide-specific-usage) for a +simpler debug story. + +## IDE Specific Usage + +Some additional tips for developing in specific IDEs. + +### Intellij IDEA / WebStorm + +To load the project in Intellij products, simply `Open` the repository folder. +Do **not** `Import Project`, because that will overwrite the existing +configuration. + +Once opened, the editor should automatically detect run configurations in the +workspace. Use the drop down to choose which one to run and then click the `Run` +button to start it. When executing a debug target, make sure to click the +`Debug` icon to automatically attach the debugger (if you click `Run`, Node will +wait forever for a debugger to attach). + +![Intellij IDEA run configurations](images/run-configurations.png) + +### VS Code + +In order to debug some Angular CLI behaviour using Visual Studio Code, you can run `npm run build`, and then use a launch configuration like the following: + +```json +{ + "type": "node", + "request": "launch", + "name": "ng serve", + "cwd": "", + "program": "${workspaceFolder}/dist/@angular/cli/bin/ng", + "args": [ + "", + ...other arguments + ], + "console": "integratedTerminal" +} +``` + +Then you can add breakpoints in `dist/@angular` files. + +For more information about Node.js debugging in VS Code, see the related [VS Code Documentation](https://code.visualstudio.com/docs/nodejs/nodejs-debugging). + +## CPU Profiling + +In order to investigate performance issues, CPU profiling is often useful. + +### Creating a profile + +Node.js 16+ users can use the Node.js command line argument `--cpu-prof` to create a CPU profile. + +```bash +node --cpu-prof node_modules/.bin/ng build +``` + +In addition to this one, another, more elaborated way to capture a CPU profile using the Chrome Devtools is detailed in https://github.com/angular/angular-cli/issues/8259#issue-269908550. + +#### Opening a profile + +You can use the Chrome Devtools to process it. To do so: + +1. open `chrome://inspect` in Chrome +1. click on "Open dedicated DevTools for Node" +1. go to the "profiler" tab +1. click on the "Load" button and select the generated `.cpuprofile` file +1. on the left panel, select the associated file + +## Creating New Packages + +Adding a package to this repository means running two separate commands: + +1. `schematics devkit:package PACKAGE_NAME`. This will update the `.monorepo` file, and create the + base files for the new package (package.json, src/index, etc). +1. `devkit-admin templates`. This will update the README and all other template files that might + have changed when adding a new package. + +For private packages, you will need to add a `"private": true` key to your package.json manually. +This will require re-running the template admin script. + +## Windows + +To contribute to Angular using a Windows machine, you need to use the [Windows Linux Subsystem](https://learn.microsoft.com/en-us/windows/wsl/about) (also known as WSL). +Installing WSL on your machine requires a few extra steps, but we believe it's generally useful for developing on Windows: + +1. Run `wsl --install` from Powershell (as administrator). +2. Restart your machine. +3. Enter the WSL environment by running: `wsl`. +4. Continue with the developer guide as if you were on a native Linux system. + +For a more detailed guide, refer to the official Microsoft documentation: [Installing WSL](https://learn.microsoft.com/en-us/windows/wsl/install). + +**Note:** Angular continues to support native Windows development via the `ng` CLI and rigorously tests on Windows for every code change. This recommendation specifically applies to contributing to the Angular codebase itself. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000000..1aa9fc8f9262 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,17 @@ +# `/docs` Folder + +This folder is used for all documentation. It contains a number of subfolders with short +descriptions here. + +## `/docs/design` + +Design documents on GitHub, in Markdown format. The number of design documents available is limited. + +## `/docs/process` + +Description of various caretaker and workflow processes. + +## `/docs/specifications` + +Specifications for support of Angular CLI features. These are meant to be read directly on GitHub +by developers of libraries who want to integrate with the Angular CLI. diff --git a/docs/design/analytics.md b/docs/design/analytics.md new file mode 100644 index 000000000000..0eb3bad26d8f --- /dev/null +++ b/docs/design/analytics.md @@ -0,0 +1,101 @@ +# Usage Metrics Gathering + +This document list exactly what is gathered and how. + +Any change to analytics should most probably include a change to this document. + +## Dimensions and Metrics + +Google Analytics Custom Dimensions are used to track system values and flag values. These +dimensions are aggregated automatically on the backend. + +One dimension per flag, and although technically there can be an overlap between 2 commands, for +simplicity it should remain unique across all CLI commands. The dimension is the value of the +`x-user-analytics` field in the `schema.json` files. + +### Adding dimension or metic. +1. Create the dimension or metric in [Google Analytics](https://analytics.google.com/) first. These are not tracked if they aren't + defined in Google Analytics. +1. Use the ID of the dimension as the `x-user-analytics` value in the `schema.json` file. +1. New dimension and metrics PRs need to be approved by the tooling lead and require a new [Launch](http://go/launch). + +### Deleting a dimension or metic. +1. Archive the dimension and metric in [Google Analytics](https://analytics.google.com/). + + +**DO NOT ADD `x-user-analytics` FOR VALUES THAT ARE USER IDENTIFIABLE (PII), FOR EXAMPLE A +PROJECT NAME TO BUILD OR A MODULE NAME.** + +### Limits +| Item | Standard property limits | +|-------------------------------- |-------------------------- | +| Event-scoped custom dimensions | 50 | +| User-scoped custom dimensions | 25 | +| All custom metrics | 50 | + +### List of User Custom Dimensions + + +| Name | Parameter | Type | +|:---:|:---|:---| +| UserId | `up.ng_user_id` | `string` | +| OsArchitecture | `up.ng_os_architecture` | `string` | +| NodeVersion | `up.ng_node_version` | `string` | +| NodeMajorVersion | `upn.ng_node_major_version` | `number` | +| AngularCLIVersion | `up.ng_cli_version` | `string` | +| AngularCLIMajorVersion | `upn.ng_cli_major_version` | `number` | +| PackageManager | `up.ng_package_manager` | `string` | +| PackageManagerVersion | `up.ng_pkg_manager_version` | `string` | +| PackageManagerMajorVersion | `upn.ng_pkg_manager_major_v` | `number` | + + +### List of Event Custom Dimensions + + +| Name | Parameter | Type | +|:---:|:---|:---| +| Command | `ep.ng_command` | `string` | +| SchematicCollectionName | `ep.ng_schematic_collection_name` | `string` | +| SchematicName | `ep.ng_schematic_name` | `string` | +| Standalone | `ep.ng_standalone` | `string` | +| SSR | `ep.ng_ssr` | `string` | +| Style | `ep.ng_style` | `string` | +| Routing | `ep.ng_routing` | `string` | +| InlineTemplate | `ep.ng_inline_template` | `string` | +| InlineStyle | `ep.ng_inline_style` | `string` | +| BuilderTarget | `ep.ng_builder_target` | `string` | +| Aot | `ep.ng_aot` | `string` | +| Optimization | `ep.ng_optimization` | `string` | + + +### List of Event Custom Metrics + + +| Name | Parameter | Type | +|:---:|:---|:---| +| AllChunksCount | `epn.ng_all_chunks_count` | `number` | +| LazyChunksCount | `epn.ng_lazy_chunks_count` | `number` | +| InitialChunksCount | `epn.ng_initial_chunks_count` | `number` | +| ChangedChunksCount | `epn.ng_changed_chunks_count` | `number` | +| DurationInMs | `epn.ng_duration_ms` | `number` | +| CssSizeInBytes | `epn.ng_css_size_bytes` | `number` | +| JsSizeInBytes | `epn.ng_js_size_bytes` | `number` | +| NgComponentCount | `epn.ng_component_count` | `number` | +| AllProjectsCount | `epn.all_projects_count` | `number` | +| LibraryProjectsCount | `epn.libs_projects_count` | `number` | +| ApplicationProjectsCount | `epn.apps_projects_count` | `number` | + + +## Debugging + +Using `NG_DEBUG=1` will enable Google Analytics debug mode, To view the debug events, in Google Analytics go to `Configure > DebugView`. + +## Disabling Usage Analytics + +There are 2 ways of disabling usage analytics: + +1. using `ng analytics disable --global` (or changing the global configuration file yourself). This is the same + as answering "No" to the prompt. +1. There is an `NG_CLI_ANALYTICS` environment variable that overrides the global configuration. + That flag is a string that represents the User ID. If the string `"false"` is used it will + disable analytics for this run. diff --git a/docs/design/build-system-overview.dot b/docs/design/build-system-overview.dot new file mode 100644 index 000000000000..e792d7b42d81 --- /dev/null +++ b/docs/design/build-system-overview.dot @@ -0,0 +1,15 @@ +digraph G { + node [shape=rectangle]; + "assets array" -> "copy-webpack-plugin"; + "*.ts" -> "@ngtools/webpack" -> "@angular-devkit/build-optimizer"; + "*.js" -> "source-map-loader" -> "@angular-devkit/build-optimizer"; + "@angular-devkit/build-optimizer" -> "webpack module concatenation" -> "webpack bundling" -> "terser-webpack-plugin"; + "scripts array" -> "terser-webpack-plugin"; + "*.css" -> "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer"; + "*.scss\|sass" -> "sass-loader" -> "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer"; + "*.less" -> "less-loader" -> "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer"; + "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer" -> "raw-loader, ./optimize-css-webpack-plugin.ts" [label="component style?"]; + "raw-loader" -> "./optimize-css-webpack-plugin.ts" + "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer" -> "style-loader, ./raw-css-loader.ts, and mini-css-extract-plugin" [label="global style?"]; + "style-loader, ./raw-css-loader.ts, and mini-css-extract-plugin" -> "./optimize-css-webpack-plugin.ts" +} diff --git a/docs/design/build-system.md b/docs/design/build-system.md new file mode 100644 index 000000000000..d4d1d15b33bc --- /dev/null +++ b/docs/design/build-system.md @@ -0,0 +1,194 @@ +# Build System + +Angular CLI includes a first-party build system for Angular applications distributed as `@angular-devkit/build-angular`. +This build system is responsible for creating a standalone single-page application (SPA) from user source files and third-party dependencies. + +`@angular-devkit/build-angular` itself integrates with the rest of Angular CLI by being an [Architect builder](https://angular.dev/tools/cli/cli-builder). +This document describes a top level view of the functionality in `@angular-devkit/build-angular`, referred as just the "build system". +Deprecated or soon to be removed features are not described here. + +In broad strokes the main areas are: + +- loading and processing sources +- code splitting +- production optimizations +- post-processing steps + +Many tools are used in this process, and most of these steps happen within a [Webpack](https://webpack.js.org/) build. +We maintain a number of Webpack-centric plugins in this repository, some of these are public but most are private since they are very specific to our setup. + +## Overview diagram + +Below is a diagram of processing sources go through. +It's not strictly accurate because some options remove certain processing stages while adding others. +This diagram doesn't show these conditionals and only shows the possible processing steps. +Relative paths, such as `./raw-css-loader.ts`, refer to internal plugins, while other names usually refer to public npm packages. + +![Overview diagram](https://g.gravizo.com/source/svg?https://raw.githubusercontent.com/angular/angular-cli/main/docs/design/build-system-overview.dot) + +## Loading and processing sources + +Sources for Angular CLI browser apps are comprised of TypeScript files, style sheets, assets, scripts, and third party dependencies. +A given build will load these sources from disk, process them, and bundle them together. + +### TypeScript and Ahead-Of-Time Compilation + +Angular builds rely heavily on TypeScript-specific functionality for [Ahead-of-Time template compilation](https://angular.dev/tools/cli/aot-compiler) (AOT). +Outside Angular CLI, this is performed by the Angular Compiler (`ngc`), provided by `@angular/compiler-cli`. +To avail of Ahead-of-Time template compilation within a Webpack compilation we use and distribute the `@ngtools/webpack` Webpack plugin. + +Typescript sources are loaded from disk and compiled in-memory into JavaScript files that are stored in a virtual file system and made available to Webpack. +During compilation we also perform a number of code transformations using TypeScript transformers that enable automatic usage of AOT, internationalization features, and server-side rendering. + +AOT compilation requires loading HTML and CSS resources, referenced on Angular Components, as standalone strings with no external dependencies. +However, Webpack compilations operate on the basis of modules and references between them. +It's not possible to get the full compilation result for a given resource before the end of the compilation in webpack. +To obtain the standalone string for a HTML/CSS resource we compile it using a separate Webpack child compilation then extract the results. +These child compilations inherit configuration and access to the same files as the parent compilation, but have their own compilation life cycle and complete independently. + +The build system allows specifying replacements for specific files by replacing what path is loaded from the virtual file system. +This is used for conditional loading of code at build time. + +### Stylesheets + +Two types of stylesheets are used in the build system: global stylesheets and component stylesheets. +Global stylesheets are injected into the `index.html` file, while component stylesheets are loaded directly into compiled Angular components. + +The build system supports plain CSS stylesheets as well as the Sass and LESS CSS pre-processors. +Stylesheet processing functionality is provided by `sass-loader`, `less-loader`, `postcss-loader`, `postcss-import`, augmented in the build system by custom webpack plugins. + +### Assets + +Assets in the build system refer specifically to a list of files or directories that are meant to be copied verbatim as build artifacts. +These files are not processed and commonly include images, favicons, pdfs and other generic file types. +They are loaded into the compilation using `copy-webpack-plugin`. + +### Scripts + +Scripts in the build system refer specifically to JavaScript files that are meant to be loaded directly on `index.html` without being processed. +They are loaded into the compilation using a custom webpack plugin. + +### Third party dependencies + +Third party dependencies are mostly inside `node_modules` and are referenced via imports in source files. +Stylesheet third party dependencies are treated mostly the same as sources. + +JavaScript third party dependencies suffer a more involved process. +They are first resolved to a folder in `node_modules` via [Node Module Resolution](https://nodejs.org/api/modules.html#modules_modules). +A given module might have several different entry points, for instance one for use in NodeJS and another one for using in the browser. +Although `package.json` only officially supports listing one entry point under the `main` key, it's common for npm packages to list [other entry points](https://2ality.com/2017/04/setting-up-multi-platform-packages.html). +Each entry point is listed in under a key in that module's `package.json` whose value is a string containing a relative file path. +We use `es2015 > browser > module > main` a priority list of keys, where the first key matched name determines which entry point to use. +For instance, for a module that has both `browser` and `main` entry points, we pick `browser`. + +Once the actual JavaScript file is determined, it is loaded into the compilation together with it's source map. + +This resolution strategy supports the [Angular Package Format](https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#heading=h.k0mh3o8u5hx). + +## Code splitting + +Code is automatically split into different files (or chunks, for js files) based on a few different triggers. + +The main TypeScript entry point and it's dependencies are bundled into the `main` chunk. +Global styles and scripts get one file per entry, named after themselves. + +JavaScript code imported only via dynamic imports is automatically split into a separate chunk that is loaded asynchronously named after the file containing the dynamic import. +If multiple asynchronous chunks contain a reference to the same module, it is placed in a new asynchronously loaded chunk named after the other chunks that use it. + +There is also a special chunk called `runtime` that contains the module loading logic and is loaded before the others. + +## Optimizations + +The build system contains optimizations aimed at improving the performance (for development builds) or the size of artifacts (for production builds). +These are often mutually exclusive and thus we cannot just default to always using them. + +### Development optimizations + +Development optimizations focus on reducing rebuild time on watched builds. +Although faster is always better, our threshold is to keep rebuilds even for large projects below 2 seconds. + +Computation needed to bundle code grows with its total size because of the cost of string concatenation and source map operations. +Third party dependencies that are initially loaded are split into a synchronously loaded chunk called `vendor`. +Splitting the infrequently changed vendor code from the frequently changed source code thus helps make rebuilds faster. + +When processing stylesheets, Webpack stores the intermediate modules as JavaScript code. +The JavaScript wrapper code makes stylesheets larger and `mini-css-extract-plugin` must be used to obtain the actually stylesheet content into a CSS file. +In development however, we skip the CSS extraction and leave it as JavaScript code for faster rebuild times. + +Watched builds split the processing load of TypeScript compilation between file emission on the main process and type checking on a forked process. +Large projects can also opt-out of AOT compilation for faster rebuilds. + +### Production optimizations + +Angular CLI focuses on enabling tree-shaking (removing unused modules) and dead code elimination (removing unused module code). +These two categories have high potential for size reduction because of network effects: removing code can lead to more code being removed. + +The main tool we use to achieve this goal are the dead code elimination capabilities of [Terser](https://github.com/terser/terser). +We also use Terser's mangling, by which names, but not properties, are renamed to shorter forms. +The main characteristics of Terser to keep in mind is that it operates via static analysis and does not support the indirection introduced by module loading. +Thus the rest of the pipeline is directed towards providing Terser with code that can be removed via static analysis in large single modules scopes. + +To this end we developed [@angular-devkit/build-optimizer](https://github.com/angular/angular-cli/tree/main/packages/angular_devkit/build_optimizer), a post-processing tool for TS code. +Build Optimizer searches for code patterns produced by the TypeScript and Angular compiler that are known to inhibit dead code elimination, and converts them into equivalent structures that enable it instead (the link above contains some examples). +It also adds Terser [annotations](https://github.com/terser/terser#annotations) marking top-level functions as free from side effects for libraries that have the `sideEffects` flag set to false in `package.json`. + +Webpack itself also contains two major features that enable tree-shaking and dead code elimination: [`sideEffects` flag](https://github.com/webpack/webpack/tree/master/examples/side-effects) support and [module concatenation](https://webpack.js.org/plugins/module-concatenation-plugin/). +Having the `sideEffects` flag set to false in `package.json` of a library means that library has no top-level side-effects and only exposes imports, which allows Webpack to rewrite imports to that library directly to the modules used and not including non-imported modules at all. +Module concatenation allows Webpack to collect in a single module the content of several modules, which in turn allows Terser to more easily remove unused code since there is no module loading indirection between those modules. + +One significant pitfall of this optimization strategy is the use of code splitting. +Using code splitting is desirable in order to speed up loading of web apps by deferring code that is not necessary on the initial load. +But since code splitting necessarily makes use of module loading, it is at odds with Terser-based optimizations. + +The use of lazy loading can not only prevent further optimizations, but also regress the currently possible ones by [preventing module concatenation](https://webpack.js.org/plugins/module-concatenation-plugin/#optimization-bailouts). +Modules that were concatenated when lazy modules are not present might not be concatenated anymore after lazy loading is introduced because these modules now need to be accessed from the lazy modules and thus get their own module scope. + +Aside from tree-shaking, scripts and styles (as defined in the sources above) also undergo optimizations via [Terser](https://github.com/terser/terser) and [CleanCSS](https://github.com/jakubpawlowicz/clean-css) respectively. + +## Post-processing steps + +There are some steps that are meant to operate over whole applications and thus happen after the compilation finishes and outputs files. +The steps are described in the order in which they are executed during a build. +The execution order was determined based on the complexity of the step with a primary goal of minimizing the repetition of computationally expensive operations. + +### Differential Loading + +Differential loading is a strategy that allows your web application to support multiple browsers, but only load the necessary code that the browser needs. +When differential loading is enabled, the CLI generates two distinct variants of application bundles. + +- The first contains ES2015 syntax, takes advantage of built-in support in modern browsers, ships fewer polyfills, and results in a smaller total size. +- The second contains code in the older ES5 syntax, along with all necessary polyfills for Angular to function. This results in a larger total size, but supports older browsers. + +This process as designed has the advantage that only one full compilation of the application in ES2015 syntax is required. +This removes a large amount of otherwise unnecessary and duplicate processing such as module resolution and dead code elimination. +It also guarantees that the application is ES5 compliant including third-party code. +The two variants of application bundles are created by the following steps: + +1. A full build of the application is performed using an ES2015 output target. + The application's global stylesheets, scripts, and assets are also processed during this step via the full build. These elements are reused for both of application variants. +2. A copy of the JavaScript output application files are transformed (commonly referred to as down-leveled) to ES5 compatible syntax. + ES2015+ syntax elements such as classes are converted into functionally equivalent ES5 code structures. +3. An additional ES5-only polyfills file is generated that contains the required Angular polyfills for ES5-only browsers. +4. A single index HTML file is created that references both application variants and is designed to only load the appropriate files for each browser. + +To support loading the file sets in the appropriate browsers, the HTML `script` element's `type` and `nomodule` attributes are leveraged. +Browsers will only load a script with a known type. +The ES2015 files are referenced using a type of `module` which is only supported on browsers that support ES2015+ code. +Since browsers that do not support ES2015+ code also do not support the `module` script type, these scripts are ignored for browsers that cannot parse and execute the ES2015 code. +Browsers that support the `module` script type also support the `nomodule` attribute. +This attribute instructs a browser that supports module scripts to ignore the script with the attribute. There is one browser exception in this case: Safari 10.1. +This browser supports module scripts but does not support the `nomodule` attribute. +To support this case, a special polyfill script is included to provide a workaround for the browser. +This arrangement of script elements ensures that ES5-only browsers will only execute the ES5 script files and browsers that support ES2015+ will only execute the ES2015 script files. + +### Localization (i18n) + +The second of these post-processing steps is build-time localization. +The final js bundles are processed using `@angular/localize`, replacing any locale-specific translations. +This sort of localization produces one application for each locale, each in their own folders. + +### Service Worker + +The third and last post-processing step is the creation of a [service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API). +A listing of final application files is taken, fingerprinted according to their content, and added to the service worker manifest. +This must be the last step because it needs each application file to not be modified further. diff --git a/docs/design/deployurl-basehref.md b/docs/design/deployurl-basehref.md index 160975423aeb..71a804c073e4 100644 --- a/docs/design/deployurl-basehref.md +++ b/docs/design/deployurl-basehref.md @@ -1,18 +1,18 @@ -| | Deploy URL | Base HREF | -|--|:--:|:--:| -| Initial scripts (index.html) | ✅ 👍 | ✅ 👍 | -| Initial stylesheets (index.html) | ✅ 👍 | ✅ 👍 | -| Lazy scripts (routes/import()/etc.) | ✅ 👍 | ✅ 👍 | -| Processed CSS resources (images/fonts/etc.) | ✅ 👍 | ✅ 👍 | -| Relative template (HTML) assets | ❌ 👎 | ✅ 👍 | -| Angular Router Default Base (APP_BASE_HREF) | ❌ | ✅ *1 | -| Single reference in deployed Application | ❌ 👎 | ✅ 👍 | -| Special resource logic within CLI | ✅ 👎 | ❌ 👍 | -| Relative fetch/XMLHttpRequest | ❌ | ✅ | +| | Deploy URL | Base HREF | +| ------------------------------------------- | :--------: | :-------: | +| Initial scripts (index.html) | ✅ 👍 | ✅ 👍 | +| Initial stylesheets (index.html) | ✅ 👍 | ✅ 👍 | +| Lazy scripts (routes/import()/etc.) | ✅ 👍 | ✅ 👍 | +| Processed CSS resources (images/fonts/etc.) | ✅ 👍 | ✅ 👍 | +| Relative template (HTML) assets | ❌ 👎 | ✅ 👍 | +| Angular Router Default Base (APP_BASE_HREF) | ❌ | ✅ \*1 | +| Single reference in deployed Application | ❌ 👎 | ✅ 👍 | +| Special resource logic within CLI | ✅ 👎 | ❌ 👍 | +| Relative fetch/XMLHttpRequest | ❌ | ✅ | ✅ - has/affects the item/trait ❌ - does not have/affect the item/trait 👍 - favorable behavior 👎 - unfavorable behavior -*1 -- Users with more complicated setups may need to manually configure the `APP_BASE_HREF` token within the application. (e.g., application routing base is `/` but assets/scripts/etc. are at `/assets/`) \ No newline at end of file +\*1 -- Users with more complicated setups may need to manually configure the `APP_BASE_HREF` token within the application. (e.g., application routing base is `/` but assets/scripts/etc. are at `/assets/`) diff --git a/docs/design/docker-deploy.md b/docs/design/docker-deploy.md deleted file mode 100644 index 634fdc8b3e21..000000000000 --- a/docs/design/docker-deploy.md +++ /dev/null @@ -1,454 +0,0 @@ -# Docker Deploy - Design - -## Abstract - -Add convenience for users to containerize and deploy their Angular application via Docker. - -Provide tasks for common Docker workflows: - -* Generate starter `Dockerfile` and `docker-compose.yml` files. -* Build and Push an Angular app image to a Docker Registry. -* Deploy and run an Angular app container in different Docker Machine environments. - - -## Requirements - -1. Requires user to have Docker CLI tools installed. - (See also: ["Implementation Approaches"](#implementation-approaches)) -1. User is free to use the Angular CLI without Docker (and vice versa). By default, do not generate Docker files upon creation of a new project (`ng new`). -1. Don't recreate the wheel. Docker CLI tools are very full featured on their own. Implement the common Docker use cases that make it convenient for Angular applications. -1. Don't inhibit users from using the standalone Docker CLI tools for other use cases. -1. Assumes 1:1 Dockerfile with the Angular project. Support for multiple services under the same project is outside the scope of this initial design. -1. Generated starter Dockerfile will use an Nginx base image for the default server. The built ng app and Nginx configuration for HTML5 Fallback will be copied into the image. -1. User is free to modify and customize all generated files directly without involvement by the Angular CLI. -1. Image builds will support all Docker build options. -1. Container deploys will support all Docker run options. -1. Deploying to a Docker Machine can be local or remote. -1. Deploys can be made to different environments (dev, stage, prod) on the same or different Docker Machines. -1. Image pushes can be made to Docker Hub, AWS ECR, and other public/private registries. -1. Adhere to [Docker security best practices](https://docs.docker.com/engine/security/). -1. Use sensible defaults to make it easy for users to get started. -1. Support `--dry-run` and `--verbose` flags. - - -## Proposed CLI API - -### Overview - -Initialize the project for Docker builds and deploys: -``` -$ ng docker init [--environment env] -``` - -Build and push a Docker image of the Angular app to the registry: -``` -$ ng docker push [--registry url] -``` - -Deploy and run the Angular app on a Docker Machine: -``` -$ ng docker deploy [--environment env] -``` - -### Command - Docker Init - -The command `ng docker init` generates starter `Dockerfile`, `.dockerignore`, and `docker-compose.yml` files for builds and and deploys. - -Most users will start with one local 'default' Docker Machine (Mac/Win), or a local native Docker environment on Linux, where they will perform builds and run containers for development testing. Without additional arguments, this command prepares the Angular project for working with that default environment. - -**Arguments:** - -* `--environment {env}` ; initialize for a particular environment (ie. dev, stage, prod). Defaults to `"default"`. -* `--machine {machineName}` ; choose a particular Docker Machine for this environment. Defaults to the environment name. -* `--service-name {serviceName}` ; the name for the webservice that serves the angular application. Defaults to `project.name` -* `--service-port {servicePort}` ; the service port that should be mapped on the host. Defaults to `8000`. -* `--use-image` ; initializes the environment for deploying with an image, rather than performing a build. By default, this is `false` and the `docker-compose.yml` file will be initialized for builds. -* `--image-org {orgName}` ; the org name to use for image pushes. Defaults to `null`. -* `--image-name {imageName}` ; the image name to use for image pushes. Also applies when `--use-image` is `true`. Defaults to `serviceName`. -* `--image-registry {address}` ; the registry address to use for image pushes. Defaults to `registry.hub.docker.com`. Also applies when `--use-image` is `true`. - -**Example - Init default environment:** - -```bash -$ ng docker:init --image-org my-username - -Generated 'Dockerfile' -Generated '.dockerignore' -Generated 'docker-compose.yml' - -Docker is ready! - -You can build and push a Docker image of your application to a docker registry using: - $ ng docker push - -Build and run your application using: - $ ng docker deploy -``` - -**Other requirements:** - -If the Docker CLI tools are not found, display an error with instructions on how to install Docker Toolbox. If no Docker Machines are found, display an error with instructions for creating a machine. - -The command should verify that the `machineName` is valid, and be able to distinguish whether it is a remote machine or a native local Docker environment (ie. Linux, Docker Native beta). - -A notice will be displayed for any files that already exist. No existing files will be overwritten or modified. Users are free to edit and maintain the generated files. - -If an `env` name is provided, other than "default", generate compose files with the env name appended, ie. `docker-compose-${env}.yml`. - -If `--use-image == false` and the selected machine for the environment is a Docker Swarm machine, warn the user. Docker Swarm clusters cannot use the `build:` option in compose, since the resulting built image will not be distributed to other nodes. Swarm requires using the `image:` option in compose, pushing the image to a registry beforehand so that the Swarm nodes have a place to pull the image from (see [Swarm Limitations](https://docs.docker.com/compose/swarm/#building-images)). - -Certain configuration values will be stored in the project's ngConfig `.angular-cli.json` for use with other docker commands (ie. deploy, logs, exec). See also: [Saved State](#saved-state) section. - -Provide instructions on what the user can do after initialization completes (push, deploy). - -Users who do not wish to push images to a registry should not be forced to. - -#### Example - `Dockerfile` blueprint - -```Dockerfile -# You are free to change the contents of this file -FROM nginx - -# Configure for angular fallback routes -COPY nginx.conf /etc/nginx/nginx.conf - -# Copy built app to wwwroot -COPY dist /usr/share/nginx/html -``` - -#### Example - `.dockerignore` blueprint - -``` -.git -.gitignore -.env* -node_modules -docker-compose*.yml -``` - -#### Example - `docker-compose.yml` blueprint - -For default case, when `--use-image == false`: - -```yaml -version: "2" -services: - ${serviceName}: - build: . - image: ${imageName} - ports: - - "${servicePort}:80" -``` - -When `--use-image == true`: - -```yaml -version: "2" -services: - ${serviceName}: - image: \${NG_APP_IMAGE} - ports: - - "${servicePort}:80" -``` - -The `\${NG_APP_IMAGE}` is a Compose variable, not an Angilar CLI var. It will be substituted during a `docker deploy` command with an environment variable of the desired tag. (See [Compose Variable Substitution](https://docs.docker.com/compose/compose-file/#variable-substitution) for more info) - -Separate `docker-compose-{environment}.yml` files are used to deploy to different [environments](https://docs.docker.com/compose/extends/#example-use-case), for use with the `ng docker deploy` command. - - -### Command - Docker Push - -The command `ng docker push` builds a Docker image of the Angular app from the `Dockerfile`, and pushes the image with a new tag to a registry address. - -Example - Build and push (with auth): -```bash -$ ng docker push --tag 1.0.0 --tag-latest -Building image... -Tagging "1.0.0"... -Tagging "latest" -Docker image built! bc2043fdd1e8 - -Pushing to registry... -> Enter your registry credentials -Username (username): -Password: - -Push complete! -my-username/my-ng-app:1.0.0 -my-username/my-ng-app:latest -``` - -#### Arguments - -* `--machine {machineName}` ; the Docker Machine to use for the build. Defaults to the "default" environment's `machineName`. -* `--image-org {orgName}` ; the org name to use for image pushes. Defaults to `null`. -* `--image-name {imageName}` ; the image name to use for image pushes. Defaults to `serviceName`. -* `--image-registry {address}` ; the registry address to use for image pushes. Defaults to `registry.hub.docker.com`. -* `--tag {tag}` ; the tag for the newly built image. Defaults to the current `package.json` "version" property value. -* `--tag-latest` ; optionally and additionally apply the "latest" tag. Defaults to `false`. -* `--no-cache` ; do not use cache when building the image. Defaults to `false`. -* `--force-rm` ; always remove intermediate containers. Defaults to `false`. -* `--pull` ; always attempt to pull a newer version of the image. Defaults to `false`. - -The `--no-cache`, `--force-rm`, and `--pull` are [compose build options](https://docs.docker.com/compose/reference/build/). - -Try an initial push. If an authentication failure occurs, attempt to login via `docker login`, or with `aws ecr get-login` (if the registry address matches `/\.dkr\.ecr\./`). Proxy the CLI input to the login commands. Avoid storing any authentication values in ngConfig. - -The `serviceName`, `registryAddress`, `orgName`, and `imageName` defaults will first be retrieved from ngConfig, which were saved during the initial `ng docker init` command. If any of these values do not exist, warn the user with instructions to add via `ng docker init` or `ng set name=value`. - -#### Internal Steps - -1. `docker-machine env {machineName}` (if remote) -1. Rebuild app for production -1. `docker-compose build {serviceName}` -1. `docker tag {imageName} {registryAddress}:{orgName}/{imageName}:{imageTag}` -1. `docker push {registryAddress}:{orgName}/{imageName}:{imageTag}` -1. `tag-latest == true` ? - 1. `docker tag {imageName} {registryAddress}:{orgName}/{imageName}:latest` - 1. `docker push {registryAddress}:{orgName}/{imageName}:latest` - - -### Command - Docker Deploy - -The command `ng docker deploy` will deploy an environment's compose configuration to a Docker Machine. It starts the containers in the background and leaves them running. - -Consider a command alias: `ng docker run`. - -Example - Default environment deploy: -```bash -$ ng docker deploy -Building... -Deploying to default environment... -Deploy complete! -``` - -Example - Deploying to a named environment, without builds: -```bash -$ ng docker:deploy --environment stage -$ ng docker:deploy --environment prod --tag 1.0.1 -``` - -Example - Deploying a specific service from the compose file: -```bash -$ ng docker deploy --services my-ng-app -``` - -#### Options - -* `--environment {env}` ; -* `--tag {tag}` ; The tag to use whe deploying to non-build Docker Machine environments; Defaults to `package.json` "version" property value. -* `--services {svc1} {svc2} ... {svcN}` ; -* `--no-cache` ; do not use cache when building the image. Defaults to `false`. -* `--force-rm` ; always remove intermediate containers. Defaults to `false`. -* `--pull` ; always attempt to pull a newer version of the image. Defaults to `false`. -* `--force-recreate` ; recreate containers even if their configuration and image haven't changed. Defaults to `false`. -* `--no-recreate` ; if containers already exist, don't recreate them. Defaults to `false`. - -The `--services` option allows for specific services to be deployed. By default, all services within the corresponding compose file will be deployed. - -The `--no-cache`, `--force-rm`, and `--pull` are [compose build options](https://docs.docker.com/compose/reference/build/). - -The `--force-recreate`, `--no-recreate` are [compose up options](https://docs.docker.com/compose/reference/up/). - -Successive deploys should only restart the updated services and not affect other existing running services. - -> If there are existing containers for a service, and the service’s configuration or image was changed after the container’s creation, docker-compose up picks up the changes by stopping and recreating the containers (preserving mounted volumes). To prevent Compose from picking up changes, use the --no-recreate flag. - -Use the `tag` value for the `${NG_APP_IMAGE_TAG}` environment variable substitution in the compose file. - -Use the `env` value to namespace the `--project-name` of the container set, for use with docker-compose when using different environment deploys on the same Docker Machine. (See also: [Compose overview](https://docs.docker.com/compose/reference/overview/)) - -#### Internal Steps - -1. `docker-machine env {machineName}` (if remote) -1. `--use-image == true` ? - 1. `export NG_APP_IMAGE={registryAddress}:{orgName}/{imageName}:{tag}` - 1. `docker-compose up -d [services]` -1. `--use-image == false` ? - 1. Rebuild app for production - 1. `docker-compose build {serviceName}` - 1. `docker-compose up -d [services]` - - -## Saved State - -Example ngConfig model for saved docker command state (per project): -``` -{ - docker: { - orgName: 'myusername', - imageName: 'ngapp', - registryAddress: 'registry.hub.docker.com', - environments: { - default: { - machineName: 'default', - isImageDeploy: false, - serviceName: 'ngapp' - }, - stage: { - machineName: 'stage', - isImageDeploy: true, - serviceName: 'ngapp' - } - } - } -} -``` - - -## Other Enhancements - -* Ability to list the configured deploy environments. -* Autocomplete environment names for `ng docker deploy`. -* New command wrappers for `docker-compose logs`, and `docker exec` commands. -* Create an Angular development environment image with everything packaged inside. Users can run the container with local volume mounts, and toolset will watch/rebuild for local file changes. Only requires Docker to be installed to get started with Angular development. There are some Node.js complexities to solve when adding new package.json dependencies, and many users report issues with file watchers and docker volume mounts. -* Deployment support to other container scheduling systems: Kubernetes, Marathon/Mesos, AWS ECS and Beanstalk, Azure Service Fabric, etc. - - -## Appendix - -### Implementation Approaches - -Two internal implementation approaches to consider when interfacing with Docker from Node.js: - -1. Docker Remote API via Node.js modules -1. Docker CLI tools via `child_process.exec` - -#### Docker Remote API - - - -> The API tends to be REST. However, for some complex commands, like attach or pull, the HTTP connection is hijacked to transport stdout, stdin and stderr. - -The API has been going through a high rate of change and has some awkward inconsistencies: - -> Build Image: reply is chunked into JSON objects like {"stream":"data"} and {"error":"problem"} instead of multiplexing stdout and stderr like the rest of the API. - -Example Image Build with `dockerode` module: - -```js -var Docker = require('dockerode'); -var tar = require('tar-fs'); - -var docker = new Docker({ - host: options.dockerHostIp, - port: options.dockerPort || 2375, - ca: fs.readFileSync(`${options.dockerCertPath}/ca.pem`), - cert: fs.readFileSync(`${options.dockerCertPath}/cert.pem`), - key: fs.readFileSync(`${options.dockerCertPath}/key.pem`) -}); -var buildImagePromise = Promise.denodeify(docker.buildImage); -var tarStream = tar.pack(project.root); - -buildImagePromise(tarStream, { - t: options.imageName -}).then((output) => { - var imageHash = output.match(/Successfully built\s+([a-f0-9]+)/m)[1]; - ui.writeLine(chalk.green(`Docker image built! ${imageHash}`)); -}); -``` - -Tradeoffs with this approach: - -* Does not require Docker CLI tools to be installed. -* Requires cert files for access to remote Docker Machines. -* Programmatic interface. -* Requires more configuration on the part of Angular CLI. -* Configuration imposes a learning curve on existing Docker users. -* No Docker-Compose API support. Multi-container management features would need to be duplicated. -* Maintenance effort to keep API updated. -* Dependent upon 3rd-party Docker module support, or creating our own. - -#### Execute Docker CLI Commands - -This method wraps the following Docker CLI tools with `exec()` calls, formatting the command arguments and parsing their output: - -* `docker-machine` : Used to initialize the `docker` client context for communication with a specific Docker Machine host, and for gathering host information (IP address). -* `docker` : The primary Docker client CLI. Good for pushing images to a registry. -* `docker-compose` : Manages multi-container deployments on a Docker Machine using a YAML format for defining container options. Can also be used to build images. - -Example image build with `docker-machine` and `docker-compose` CLI commands: - -```js -var execPromise = Promise.denodeify(require('child_process').exec); - -function getDockerEnv(machineName) { - return execPromise(`docker-machine env ${machineName}`) - .then((stdout) => { - let dockerEnv = {}; - stdout.split(/\r?\n/) - .forEach((line) => { - let m = line.match(/^export\s+(.+?)="(.*?)"$/); - if (m) dockerEnv[m[1]] = m[2]; - }); - return dockerEnv; - }); -} - -function buildImage() { - return getDockerEnv(options.buildMachineName) - .then((dockerEnv) => { - let execOptions = { - cwd: project.root, - env: dockerEnv - }; - return execPromise(`docker-compose build ${options.dockerServiceName}`, execOptions); - }) - .then((stdout) => { - var imageHash = stdout.match(/Successfully built\s+([a-f0-9]+)/m)[1]; - ui.writeLine(chalk.green(`Docker image built! ${imageHash}`)); - return imageHash - }); -} -``` - -Tradeoffs with this approach: - -* Requires user to manually install Docker CLI tools -* Must build interface around CLI commands, formatting the command arguments and parsing the output. -* Can leverage Docker-Compose and its configuration format for multi-container deploys. -* Configuration of build and deploy options is simplified in Angular CLI. -* User has the flexibility of switching between `ng` commands and Docker CLI tools without having to maintain duplicate configuration. -* Lower risk of Docker compatibility issues. User has control over their Docker version. - -##### Node.js Docker Modules - -Module | Created | Status | Dependencies ---- | --- | --- | --- -[dockerode](https://www.npmjs.com/package/dockerode) | Sep 1, 2013 | Active | 14 -[dockerizer](https://www.npmjs.com/package/dockerizer) | Feb 1, 2016 | New | 125 -[docker.io](https://www.npmjs.com/package/docker.io) | Jun 1, 2013 | Outdated | ? - -[List of Docker client libraries](https://docs.docker.com/engine/reference/api/remote_api_client_libraries/) - -#### Summary - -The "2. Docker CLI tools via `child_process.exec`" method is recommended based on the following: - -* The requirement of having the Docker CLI tools installed is not generally a problem, and they would likely already be installed by the majority of users using these features. -* Maintenance to Angular CLI would likely be easier using the Docker CLI, having less configuration, documentation, and updates than the Remote API method. -* Multi-container deploys is a common use-case. Utilizing the Docker Compose features, format, and documentation is a big win. -* Since this project is a CLI itself, using the Docker CLI tools isn't too far a leap. -* Users who do not use these features are not forced to install Docker CLI. Conversely, the Remote API method might incur a small penalty of installing unused NPM modules (ie. `dockerode`). - - -### Container Deployment APIs in the Wild - -* [Docker Run](https://docs.docker.com/engine/reference/run/) -* [Docker-Compose File](https://docs.docker.com/compose/compose-file/) -* [Kubernetes Pod](http://kubernetes.io/docs/api-reference/v1/definitions/#_v1_pod) -* [Marathon App](https://mesosphere.github.io/marathon/docs/rest-api.html#post-v2-apps) -* [Tutum Container](https://docs.tutum.co/v2/api/#container) -* [AWS Elastic Beanstalk/ECS Task Definition](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_defintions.html) -* [Azure Service Fabric App](https://azure.microsoft.com/en-us/documentation/articles/service-fabric-deploy-remove-applications/) -* [Heroku Docker CLI](https://github.com/heroku/heroku-docker) -* [Redspread](https://github.com/redspread/spread) -* [Docker Universal Control Plane](https://www.docker.com/products/docker-universal-control-plane) -* [Puppet Docker Module](https://github.com/garethr/garethr-docker) -* [Chef Docker Cookbook](https://supermarket.chef.io/cookbooks/docker) -* [Ansible Docker Module](http://docs.ansible.com/ansible/docker_module.html) -* [Bamboo Docker Tasks](https://confluence.atlassian.com/bamboo/configuring-the-docker-task-in-bamboo-720411254.html) -* [Freight Forwarder Manifest](http://freight-forwarder.readthedocs.org/en/latest/config/overview.html) -* [Gulp Docker Tasks](https://www.npmjs.com/package/gulp-docker) -* [Grunt Dock Tasks](https://www.npmjs.com/package/grunt-dock) -* [Robo Docker Tasks](http://robo.li/tasks/Docker/) diff --git a/docs/design/ngConfig.md b/docs/design/ngConfig.md index e37f1cf0e689..df40002ac3d5 100644 --- a/docs/design/ngConfig.md +++ b/docs/design/ngConfig.md @@ -2,13 +2,13 @@ ## Goals -Currently, a project scaffolded with the CLI have no way of specifying options and configurations affecting their projects. There are ways to affect the build (with the `angular-cli-build.js` file), but the following questions cannot be answered without actual project options: +Currently, a project scaffolded with the CLI has no way of specifying options and configurations affecting their projects. There are ways to affect the build (with the `angular-cli-build.js` file), but the following questions cannot be answered without actual project options: -* Where in my directory is my karma.conf file? -* What is my firebase database URL? -* Where is my client code? -* How can I use a different lazy-loading boundary prefix (or none at all)? -* Any other backend I want to run prior to `ng serve`? +- Where in my directory is my karma.conf file? +- What is my firebase database URL? +- Where is my client code? +- How can I use a different lazy-loading boundary prefix (or none at all)? +- Any other backend I want to run prior to `ng serve`? # Proposed Solution @@ -18,16 +18,15 @@ One solution would be to keep the data in the `package.json`. Unfortunately, the Instead of polluting the package file, a `.angular-cli.json` file will be created that contains all the values. Access to that file will be allowed to the user if he knows the structure of the file (unknown keys will be kept but ignored), and it's easy to read and write. - ## Fallback -There should be two `.angular-cli.json` files; one for the project and a general one. The general one should contain information that can be useful when scaffolding new apps, or informations about the user. +There should be two `.angular-cli.json` files; one for the project and a general one. The general one should contain information that can be useful when scaffolding new apps, or information about the user. The project `.angular-cli.json` goes into the project root. The global configuration should live at `$HOME/.angular-cli.json`. ## Structure -The structure should be defined by a JSON schema (see [here](http://json-schema.org/)). The schema will be used to generate the `d.ts`, but that file will be kept in the file system along the schema for IDEs. +The structure should be defined by a JSON schema (see [here](http://json-schema.org/)). The schema will be used to generate the `d.ts`, but that file will be kept in the file system along with the schema for IDEs. Every PR that would change the schema should include the update to the `d.ts`. @@ -39,7 +38,7 @@ Every PR that would change the schema should include the update to the `d.ts`. The new command `get` should be used to output values on the terminal. It takes a set of flags and an optional array of [paths](#path); -* `--glob` or `-g`; the path follows a glob format, where `*` can be replaced by any amount of characters and `?` by a single character. This will output `name=value` for each values matched. +- `--glob` or `-g`; the path follows a glob format, where `*` can be replaced by any amount of characters and `?` by a single character. This will output `name=value` for each values matched. Otherwise, outputs the value of the path passed in. If multiple paths are passed in, they follow the format of `name=value`. @@ -47,14 +46,14 @@ Otherwise, outputs the value of the path passed in. If multiple paths are passed The new command `set` should be used to set values in the local configuration file. It takes a set of flags and an optional array of `[path](#path)=value`; -* `--global`; sets the value in the global configuration. -* `--remove`; removes the key (no value should be passed in). +- `--global`; sets the value in the global configuration. +- `--remove`; removes the key (no value should be passed in). The schema needs to be taken into account when setting the value of the field; -* If the field is a number, the string received from the command line is parsed. `NaN` throws an error. -* If the field is an object, an error is thrown. -* If the path is inside an object but the object hasn't been defined yet, sets the object with empty values (use the schema to create a valid object). +- If the field is a number, the string received from the command line is parsed. `NaN` throws an error. +- If the field is an object, an error is thrown. +- If the path is inside an object but the object hasn't been defined yet, sets the object with empty values (use the schema to create a valid object). #### Path @@ -70,19 +69,23 @@ A model should be created that will include loading and saving the configuration **The model should be part of the project and created on the `project` object.** -That model can be used internally by the tool to get information. It will include a proxy handler that throws if an operation doesn't respect the schema. It will also sets values on globals and locals depending on which branches you access. +That model can be used internally by the tool to get information. It will include a proxy handler that throws if an operation doesn't respect the schema. It will also set values on globals and locals depending on which branches you access. A simple API would return the TypeScript interface: ```typescript class Config { // ... - get local(): ICliConfig { /* ... */ } - get global(): ICliConfig { /* ... */ } + get local(): ICliConfig { + /* ... */ + } + get global(): ICliConfig { + /* ... */ + } } ``` -The `local` and `global` getters return proxies that respect the JSON Schema defined for the Angular config. These proxies allow users to not worry about the existence of values; those values will only be created on disc when they are setted. +The `local` and `global` getters return proxies that respect the JSON Schema defined for the Angular config. These proxies allow users to not worry about the existence of values; those values will only be created on disc when they are set. Also, `local` will always defer to the same key-path in `global` if a value isn't available. If a value is set and the parent exists in `global`, it should be created to `local` such that it's saved locally to the project. The proxies only care about the end points of `local` and `global`, not the existence of a parent in either. @@ -115,14 +118,14 @@ The following stands true: ```typescript const config = new Config(/* ... */); -console.log(config.local.key1.key2.value); // 0, even if it doesn't exist. -console.log(config.local.key1.key2.value2); // 2, local overrides. -console.log(config.local.key1.key2.value3); // 3. -console.log(config.local.key1.key2.value4); // Schema's default value. +console.log(config.local.key1.key2.value); // 0, even if it doesn't exist. +console.log(config.local.key1.key2.value2); // 2, local overrides. +console.log(config.local.key1.key2.value3); // 3. +console.log(config.local.key1.key2.value4); // Schema's default value. -console.log(config.global.key1.key2.value); // 0. -console.log(config.global.key1.key2.value2); // 1, only global. -console.log(config.global.key1.key2.value3); // Schema's default value. +console.log(config.global.key1.key2.value); // 0. +console.log(config.global.key1.key2.value2); // 1, only global. +console.log(config.global.key1.key2.value3); // Schema's default value. config.local.key1.key2.value = 1; // Now the value is 1 inside the local. Global stays the same. @@ -132,7 +135,7 @@ config.local.key1.key2.value3 = 5; config.global.key1.key2.value4 = 99; // The local config stays the same. -console.log(config.local.key1.key2.value4); // 99, the global value. +console.log(config.local.key1.key2.value4); // 99, the global value. -config.save(); // Commits if there's a change to global and/or local. +config.save(); // Commits if there's a change to global and/or local. ``` diff --git a/docs/design/third-party-libraries.md b/docs/design/third-party-libraries.md index d07feeb8cd65..69ac6a7c8de5 100644 --- a/docs/design/third-party-libraries.md +++ b/docs/design/third-party-libraries.md @@ -10,18 +10,18 @@ with metadata that we expect. This document explores ways to improve on the proc The following use cases need to be supported by `ng install`: 1. Support for _any_ thirdparties, ultimately falling back to running `npm install` only and -doing nothing else, if necessary. + doing nothing else, if necessary. 1. Not a new package manager. 1. Metadata storage by thirdparties in `package.json`. This must be accessible by plugins for -the build process or even when scaffolding. + the build process or even when scaffolding. 1. Proper configuration of SystemJs in the browser. 1. SystemJS configuration managed by the user needs to be kept separated from the autogenerated -part. + part. 1. The build process right now is faulty because the `tmp/` directory used by Broccoli is -inside the project. TypeScript when using `moduleResolution: "node"` can resolve the -`node_modules` directory (since it's in an ancestor folder), which means that TypeScript -compiles properly, but the build does not copy files used by TypeScript to `dist/`. We need -to proper map the imports and move them to `tmp` before compiling, then to `dist/` properly. + inside the project. TypeScript when using `moduleResolution: "node"` can resolve the + `node_modules` directory (since it's in an ancestor folder), which means that TypeScript + compiles properly, but the build does not copy files used by TypeScript to `dist/`. We need + to proper map the imports and move them to `tmp` before compiling, then to `dist/` properly. 1. Potentially hook into scaffolding. 1. Potentially hook into the build. 1. Safe uninstall path. @@ -34,27 +34,32 @@ Here's a few stories that should work right off: $ ng new my-app && cd my-app/ $ ng install jquery ``` + ^(this makes jQuery available in the index) ```sh $ ng install angularfire2 > Please specify your Firebase database URL [my-app.firebaseio.com]: _ ``` + ^(makes firebase available and provided to the App) ```sh $ ng install angularfire2 --dbUrl=my-firebase-db.example.com ``` + ^(skip prompts for values passed on the command line) ```sh $ ng install angularfire2 --quiet ``` + ^(using `--quiet` to skip all prompts and use default values) ```sh $ ng install less ``` + ^(now compiles CSS files with less for the appropriate extensions) # Proposed Solution @@ -80,13 +85,16 @@ The `install` task will perform the following subtasks: These packages can be used to wrap libraries that we want to support but can't update easily, like Jasmine or LESS. + 1. **Install typings.** See the [Typings](#typings) section. 1. **Run `postinstall` scripts.** # Proof of Concept + A proof of concept is being developed. # Hooks + The third party library can implement hooks into the scaffolding, and the build system. The CLI's tasks will look for the proper hooks prior to running and execute them. @@ -96,12 +104,14 @@ checking for the `package['angular-cli']['hooks']['${hookName}']`. The hooks are tree, there is no guarantee for the order of execution. ## Install Hooks + The only tricky part here is install hooks. The installed package should recursively call its `(pre|post|)install` hooks only on packages that are newly installed. It should call `(pre|post|)reinstall` on those who were already installed. A map of the currently installed packages should be kept before performing `npm install`. # appData + The `angular-cli` key in the generated app should be used for Angular CLI specific data. This includes the CLI configuration itself, as well as third-parties library configuration. @@ -137,20 +147,21 @@ in the data without user interaction. The special strings `${...}` can be used t with special values in the default string values. The values are taken from the `package.json`. We use a declarative style to enforce sensible defaults and make sure developers think about -this. *In the case where developers want more complex interaction, they can use the -install/uninstall hooks to prompt users.* But 3rd party libraries developers need to be aware +this. _In the case where developers want more complex interaction, they can use the +install/uninstall hooks to prompt users._ But 3rd party libraries developers need to be aware that those hooks might not prompt users (no STDIN) and throw an error. # Providers + Adding Angular providers to the app should be seamless. The install process will create a -`providers.js` from all the providers contained in all the dependencies. The User can blacklist +`providers.js` from all the providers contained in all the dependencies. The User can disclude providers it doesn't want. The `providers.js` file will always be overwritten by the `install` / `uninstall` process. It needs to exist for toolings to be able to understand dependencies. These providers are global to the application. -In order to blacklist providers from being global, the user can use the `--no-global-providers` +In order to prevent providers from being global, the user can use the `--no-global-providers` flag during installation, or can change the dependencies by using `ng providers`. As an example: ```bash @@ -163,15 +174,18 @@ ng providers database angularfirebase2 Or, alternatively, the user can add its own providers and dependencies to its components. # Dependencies + Because dependencies are handled by `npm`, we don't have to handle it. # Typings + Typings should be added, but failure to find typings should not be a failure of installation. The user might still want to install custom typings himself in the worst case. The `typings` package can be used to install/verify that typings exist. If the typings do not exist natively, we should tell the user to install the ambient version if he wants to. # Index.html + We do not touch the `index.html` file during the installation task. The default page should link to a SystemJS configuration file that is auto generated by the CLI and not user configurable (see SystemJS below). If the user install a third party library, like jQuery, and @@ -181,6 +195,7 @@ The `index.html` also includes a section to configure SystemJS that can be manag This is separate from the generated code. # SystemJS + It is important that SystemJS works without any modifications by the user. It is also important to leave the liberty to the user to change the SystemJS configuration so that it fits their needs. @@ -190,6 +205,7 @@ being pulled from a CDN in production. During the `ng build` process for product SystemJS configuration script will be rebuilt to fetch from the CDN. # Upgrade Strategy + The upgrade process simply uses NPM. If new appData is added, it should be added manually using a migration hook for `postinstall`. diff --git a/docs/documentation/1-x/angular-cli.md b/docs/documentation/1-x/angular-cli.md deleted file mode 100644 index ec1c2716d17a..000000000000 --- a/docs/documentation/1-x/angular-cli.md +++ /dev/null @@ -1,102 +0,0 @@ - - -# Angular CLI Config Schema - -## Options - -- **project**: The global configuration of the project. - - *name* (`string`): The name of the project. - - *ejected*(`boolean`): Whether or not this project was ejected. Default is `false`. - - -- **apps** (`array`): Properties of the different applications in this project. - - *name* (`string`): Name of the app. - - *root* (`string`): The root directory of the app. - - *outDir* (`string`): The output directory for build results. Default is `dist/`. - - *assets* (`array`): List of application assets. - - *deployUrl* (`string`): URL where files will be deployed. - - *index* (`string`): The name of the start HTML file. Default is `index.html` - - *main* (`string`): The name of the main entry-point file. - - *polyfills* (`string`): The name of the polyfills entry-point file. Loaded before the app. - - *test* (`string`): The name of the test entry-point file. - - *tsconfig* (`string`): The name of the TypeScript configuration file. Default is `tsconfig.app.json`. - - *testTsconfig* (`string`): The name of the TypeScript configuration file for unit tests. - - *prefix* (`string`): The prefix to apply to generated selectors. - - *serviceWorker* (`boolean`): Experimental support for a service worker from @angular/service-worker. Default is `false`. - - *showCircularDependencies* (`boolean`): Show circular dependency warnings on builds. Default is `true`. - - *styles* (`string|array`): Global styles to be included in the build. - - *stylePreprocessorOptions* : Options to pass to style preprocessors. - - *includePaths* (`array`): Paths to include. Paths will be resolved to project root. - - *scripts* (`array`): Global scripts to be included in the build. - - *environmentSource* (`string`): Source file for environment config. - - *environments* (`object`): Name and corresponding file for environment config. - -- **e2e**: Configuration for end-to-end tests. - - *protractor* - - *config* (`string`): Path to the config file. - -- **lint** (`array`): Properties to be passed to TSLint. - - *files* (`string|array`): File glob(s) to lint. - - *project* (`string`): Location of the tsconfig.json project file. Will also use as files to lint if 'files' property not present. - - *tslintConfig* (`string`): Location of the tslint.json configuration. Default is `tslint.json`. - - *exclude* (`string|array`): File glob(s) to ignore. - - -- **test**: Configuration for unit tests. - - *karma* - - *config* (`string`): Path to the karma config file. - - *codeCoverage* - - *exclude* (`array`): Globs to exclude from code coverage. - -- **defaults**: Specify the default values for generating. - - *styleExt* (`string`): The file extension to be used for style files. - - *poll* (`number`): How often to check for file updates. - - *class*: Options for generating a class. - - *spec* (`boolean`): Specifies if a spec file is generated. Default is `false`. - - *component*: Options for generating a component. - - *flat* (`boolean`): Flag to indicate if a dir is created. Default is `false`. - - *spec* (`boolean`): Specifies if a spec file is generated. Default is `true`. - - *inlineStyle* (`boolean`): Specifies if the style will be in the ts file. Default is `false`. - - *inlineTemplate* (`boolean`): Specifies if the template will be in the ts file. Default is `false`. - - *viewEncapsulation* (`string`): Specifies the view encapsulation strategy. Can be one of `Emulated`, `Native` or `None`. - - *changeDetection* (`string`): Specifies the change detection strategy. Can be one of `Default` or `OnPush`. - - *directive*: Options for generating a directive. - - *flat* (`boolean`): Flag to indicate if a dir is created. Default is `true`. - - *spec* (`boolean`): Specifies if a spec file is generated. Default is `true`. - - *guard*: Options for generating a guard. - - *flat* (`boolean`): Flag to indicate if a dir is created. Default is `true`. - - *spec* (`boolean`): Specifies if a spec file is generated. Default is `true`. - - *interface*: Options for generating a interface. - - *prefix* (`string`): Prefix to apply to interface names. (i.e. I) - - *module*: Options for generating a module. - - *flat* (`boolean`): Flag to indicate if a dir is created. Default is `false`. - - *spec* (`boolean`): Specifies if a spec file is generated. Default is `false`. - - *pipe*: Options for generating a pipe. - - *flat* (`boolean`): Flag to indicate if a dir is created. Default is `true`. - - *spec* (`boolean`): Specifies if a spec file is generated. Default is `true`. - - *service*: Options for generating a service. - - *flat* (`boolean`): Flag to indicate if a dir is created. Default is `true`. - - *spec* (`boolean`): Specifies if a spec file is generated. Default is `true`. - - *build*: Properties to be passed to the build command. - - *sourcemaps* (`boolean`): Output sourcemaps. - - *baseHref* (`string`): Base url for the application being built. - - *progress* (`boolean`): Log progress to the console while building. Default is `true`. - - *poll* (`number`): Enable and define the file watching poll time period (milliseconds). - - *deleteOutputPath* (`boolean`): Delete output path before build. Default is `true`. - - *preserveSymlinks* (`boolean`): Do not use the real path when resolving modules. Default is `false`. - - *showCircularDependencies* (`boolean`): Show circular dependency warnings on builds. Default is `true`. - - *namedChunks* (`boolean`): Use file name for lazy loaded chunks. - - *serve*: Properties to be passed to the serve command - - *port* (`number`): The port the application will be served on. Default is `4200`. - - *host* (`string`): The host the application will be served on. Default is `localhost`. - - *ssl* (`boolean`): Enables ssl for the application. Default is `false`. - - *sslKey* (`string`): The ssl key used by the server. Default is `ssl/server.key`. - - *sslCert* (`string`): The ssl certificate used by the server. Default is `ssl/server.crt`. - - *proxyConfig* (`string`): Proxy configuration file. - -- **packageManager** (`string`): Specify which package manager tool to use. Options include `npm`, `cnpm` and `yarn`. - -- **warnings**: Allow people to disable console warnings. - - *nodeDeprecation* (`boolean`): Show a warning when the node version is incompatible. Default is `true`. - - *packageDeprecation* (`boolean`): Show a warning when the user installed angular-cli. Default is `true`. - - *versionMismatch* (`boolean`): Show a warning when the global version is newer than the local one. Default is `true`. diff --git a/docs/documentation/1-x/build.md b/docs/documentation/1-x/build.md deleted file mode 100644 index 19fe3371f891..000000000000 --- a/docs/documentation/1-x/build.md +++ /dev/null @@ -1,407 +0,0 @@ - - -# ng build - -## Overview -`ng build` compiles the application into an output directory - -### Creating a build - -```bash -ng build -``` - -The build artifacts will be stored in the `dist/` directory. - -All commands that build or serve your project, `ng build/serve/e2e`, will delete the output -directory (`dist/` by default). -This can be disabled via the `--no-delete-output-path` (or `--delete-output-path=false`) flag. - -### Build Targets and Environment Files - -`ng build` can specify both a build target (`--target=production` or `--target=development`) and an -environment file to be used with that build (`--environment=dev` or `--environment=prod`). -By default, the development build target and environment are used. - -The mapping used to determine which environment file is used can be found in `.angular-cli.json`: - -```json -"environmentSource": "environments/environment.ts", -"environments": { - "dev": "environments/environment.ts", - "prod": "environments/environment.prod.ts" -} -``` - -These options also apply to the serve command. If you do not pass a value for `environment`, -it will default to `dev` for `development` and `prod` for `production`. - -```bash -# these are equivalent -ng build --target=production --environment=prod -ng build --prod --env=prod -ng build --prod -# and so are these -ng build --target=development --environment=dev -ng build --dev --e=dev -ng build --dev -ng build -``` - -You can also add your own env files other than `dev` and `prod` by doing the following: -- create a `src/environments/environment.NAME.ts` -- add `{ "NAME": 'src/environments/environment.NAME.ts' }` to the `apps[0].environments` object in `.angular-cli.json` -- use them via the `--env=NAME` flag on the build/serve commands. - -### Base tag handling in index.html - -When building you can modify base tag (``) in your index.html with `--base-href your-url` option. - -```bash -# Sets base tag href to /myUrl/ in your index.html -ng build --base-href /myUrl/ -ng build --bh /myUrl/ -``` - -### Bundling & Tree-Shaking - -All builds make use of bundling and limited tree-shaking, while `--prod` builds also run limited -dead code elimination via UglifyJS. - -### `--dev` vs `--prod` builds - -Both `--dev`/`--target=development` and `--prod`/`--target=production` are 'meta' flags, that set other flags. -If you do not specify either you will get the `--dev` defaults. - -Flag | `--dev` | `--prod` ---- | --- | --- -`--aot` | `false` | `true` -`--environment` | `dev` | `prod` -`--output-hashing` | `media` | `all` -`--sourcemaps` | `true` | `false` -`--extract-css` | `false` | `true` -`--named-chunks`   | `true` | `false` -`--build-optimizer` | `false` | `true` with AOT and Angular 5 - -`--prod` also sets the following non-flaggable settings: -- Adds service worker if configured in `.angular-cli.json`. -- Replaces `process.env.NODE_ENV` in modules with the `production` value (this is needed for some libraries, like react). -- Runs UglifyJS on the code. - -### `--build-optimizer` and `--vendor-chunk` - -When using Build Optimizer the vendor chunk will be disabled by default. -You can override this with `--vendor-chunk=true`. - -Total bundle sizes with Build Optimizer are smaller if there is no separate vendor chunk because -having vendor code in the same chunk as app code makes it possible for Uglify to remove more unused -code. - -### CSS resources - -Resources in CSS, such as images and fonts, will be copied over automatically as part of a build. -If a resource is less than 10kb it will also be inlined. - -You'll see these resources be outputted and fingerprinted at the root of `dist/`. - -### Service Worker - -There is experimental service worker support for production builds available in the CLI. -To enable it, run the following commands: -``` -npm install @angular/service-worker --save -ng set apps.0.serviceWorker=true -``` - -On `--prod` builds a service worker manifest will be created and loaded automatically. -Remember to disable the service worker while developing to avoid stale code. - -Note: service worker support is experimental and subject to change. - -### ES2015 support - -To build in ES2015 mode, edit `./tsconfig.json` to use `"target": "es2015"` (instead of `es5`). - -This will cause application TypeScript and Uglify be output as ES2015, and third party libraries -to be loaded through the `es2015` entry in `package.json` if available. - -Be aware that JIT does not support ES2015 and so you should build/serve your app with `--aot`. -See https://github.com/angular/angular-cli/issues/7797 for details. - -## Options -
- aot -

- --aot default value: false -

-

- Build using Ahead of Time compilation. -

-
- -
- app -

- --app (aliases: -a) -

-

- Specifies app name or index to use. -

-
- -
- base-href -

- --base-href (aliases: -bh) -

-

- Base url for the application being built. -

-
- -
- deploy-url -

- --deploy-url (aliases: -d) -

-

- URL where files will be deployed. -

-
- -
- environment -

- --environment (aliases: -e) -

-

- Defines the build environment. -

-
- -
- extract-css -

- --extract-css (aliases: -ec) -

-

- Extract css from global styles onto css files instead of js ones. -

-
- -
- i18n-file -

- --i18n-file -

-

- Localization file to use for i18n. -

-
- -
- i18n-format -

- --i18n-format -

-

- Format of the localization file specified with --i18n-file. -

-
- -
- locale -

- --locale -

-

- Locale to use for i18n. -

-
- -
- missing-translation -

- --missing-translation -

-

- How to handle missing translations for i18n. -

-

- Values: error, warning, ignore -

-
- -
- output-hashing -

- --output-hashing (aliases: -oh) -

-

- Define the output filename cache-busting hashing mode. -

-

- Values: none, all, media, bundles -

-
- -
- output-path -

- --output-path (aliases: -op) -

-

- Path where output will be placed. -

-
- -
- delete-output-path -

- --delete-output-path (aliases: -dop) default value: true -

-

- Delete the output-path directory. -

-
- -
- poll -

- --poll -

-

- Enable and define the file watching poll time period (milliseconds). -

-
- -
- progress -

- --progress (aliases: -pr) default value: true inside TTY, false otherwise -

-

- Log progress to the console while building. -

-
- -
- sourcemap -

- --sourcemap (aliases: -sm, sourcemaps) -

-

- Output sourcemaps. -

-
- -
- stats-json -

- --stats-json -

-

- Generates a stats.json file which can be analyzed using tools such as: webpack-bundle-analyzer or https://webpack.github.io/analyse. -

-
- -
- target -

- --target (aliases: -t, -dev, -prod) default value: development -

-

- Defines the build target. -

-
- -
- vendor-chunk -

- --vendor-chunk (aliases: -vc) default value: true -

-

- Use a separate bundle containing only vendor libraries. -

-
- -
- common-chunk -

- --common-chunk (aliases: -cc) default value: true -

-

- Use a separate bundle containing code used across multiple bundles. -

-
- -
- verbose -

- --verbose (aliases: -v) default value: false -

-

- Adds more details to output logging. -

-
- -
- watch -

- --watch (aliases: -w) -

-

- Run build when files change. -

-
- -
- show-circular-dependencies -

- --show-circular-dependencies (aliases: -scd) -

-

- Show circular dependency warnings on builds. -

-
- -
- build-optimizer -

- --build-optimizer -

-

- Enables @angular-devkit/build-optimizer optimizations when using `--aot`. -

-
- -
- named-chunks -

- --named-chunks (aliases: -nc) -

-

- Use file name for lazy loaded chunks. -

-
- -
- bundle-dependencies -

- --bundle-dependencies -

-

- In a server build, state whether `all` or `none` dependencies should be bundles in the output. -

-
- - -
- extract-licenses -

- --extract-licenses default value: true -

-

- Extract all licenses in a separate file, in the case of production builds only. -

-
diff --git a/docs/documentation/1-x/config.md b/docs/documentation/1-x/config.md deleted file mode 100644 index b114b60c0d7d..000000000000 --- a/docs/documentation/1-x/config.md +++ /dev/null @@ -1,20 +0,0 @@ - - -# ng config - -## Overview -`ng config [key] [value]` Get/set configuration values. -`[key]` should be in JSON path format. Example: `a[3].foo.bar[2]`. -If only the `[key]` is provided it will get the value. -If both the `[key]` and `[value]` are provided it will set the value. - -## Options -
- global -

- --global default value: false -

-

- Get/set the value in the global configuration (in your home directory). -

-
diff --git a/docs/documentation/1-x/doc.md b/docs/documentation/1-x/doc.md deleted file mode 100644 index f6ea8b2d8d33..000000000000 --- a/docs/documentation/1-x/doc.md +++ /dev/null @@ -1,18 +0,0 @@ - - -# ng doc - -## Overview -`ng doc [search term]` Opens the official Angular API documentation for a given keyword on [angular.io](https://angular.io). - -## Options - -
- search -

- --search (alias: -s) default value: false -

-

- Search for the keyword in the whole [angular.io](https://angular.io) documentation instead of just the API. -

-
diff --git a/docs/documentation/1-x/e2e.md b/docs/documentation/1-x/e2e.md deleted file mode 100644 index ebfaf64e8d96..000000000000 --- a/docs/documentation/1-x/e2e.md +++ /dev/null @@ -1,81 +0,0 @@ - - -# ng e2e - -## Overview -`ng e2e` serves the application and runs end-to-end tests - -### Running end-to-end tests - -```bash -ng e2e -``` - -End-to-end tests are run via [Protractor](https://angular.github.io/protractor/). - -## Options - -Please note that options that are supported by `ng serve` are also supported by `ng e2e` - -
- config -

- --config (aliases: -c) -

-

- Use a specific config file. Defaults to the protractor config file in .angular-cli.json. -

-
- -
- element-explorer -

- --element-explorer (aliases: -ee) default value: false -

-

- Start Protractor's Element Explorer for debugging. -

-
- -
- serve -

- --serve (aliases: -s) default value: true -

-

- Compile and Serve the app. All serve options are also available. The live-reload option defaults to false, and the default port will be random. -

-

- NOTE: Build failure will not launch the e2e task. You must first fix error(s) and run e2e again. -

-
- -
- specs -

- --specs (aliases: -sp) default value: [] -

-

- Override specs in the protractor config. Can send in multiple specs by repeating flag (ng e2e --specs=spec1.ts --specs=spec2.ts). -

-
- -
- suite -

- --suite (aliases: -su) -

-

- Override suite in the protractor config. Can send in multiple suite by comma separated values (ng e2e --suite=suiteA,suiteB). -

-
- -
- webdriver-update -

- --webdriver-update (aliases: -wu) default value: true -

-

- Try to update webdriver. -

-
diff --git a/docs/documentation/1-x/eject.md b/docs/documentation/1-x/eject.md deleted file mode 100644 index d1c55e34a6a0..000000000000 --- a/docs/documentation/1-x/eject.md +++ /dev/null @@ -1,233 +0,0 @@ - - -# ng eject - -## Overview -`ng eject` ejects your app and output the proper webpack configuration and scripts. - -This command uses the same flags as `ng build`, generating webpack configuration to match those -flags. - -You can use `--force` to overwrite existing configurations. -You can eject multiple times, to have a dev and prod config for instance, by renaming the ejected -configuration and using the `--force` flag. - -### Ejecting the CLI - -```bash -ng eject -``` - -## Options -
- aot -

- --aot -

-

- Build using Ahead of Time compilation. -

-
- -
- app -

- --app (aliases: -a) default value: 1st app -

-

- Specifies app name to use. -

-
- -
- base-href -

- --base-href (aliases: -bh) -

-

- Base url for the application being built. -

-
- -
- deploy-url -

- --deploy-url (aliases: -d) -

-

- URL where files will be deployed. -

-
- -
- environment -

- --environment (aliases: -e) -

-

- Defines the build environment. -

-
- -
- extract-css -

- --extract-css (aliases: -ec) -

-

- Extract css from global styles onto css files instead of js ones. -

-
- -
- force -

- --force default value: false -

-

- Overwrite any webpack.config.js and npm scripts already existing. -

-
- -
- i18n-file -

- --i18n-file -

-

- Localization file to use for i18n. -

-
- -
- i18n-format -

- --i18n-format -

-

- Format of the localization file specified with --i18n-file. -

-
- -
- locale -

- --locale -

-

- Locale to use for i18n. -

-
- -
- missing-translation -

- --missing-translation -

-

- How to handle missing translations for i18n. -

-

- Values: error, warning, ignore -

-
- -
- output-hashing -

- --output-hashing (aliases: -oh) default value: -

-

- Define the output filename cache-busting hashing mode. Possible values: none, all, media, bundles -

-
- -
- output-path -

- --output-path (aliases: -op) default value: -

-

- Path where output will be placed. -

-
- -
- poll -

- --poll -

-

- Enable and define the file watching poll time period (milliseconds) . -

-
- -
- progress -

- --progress (aliases: -pr) default value: true inside TTY, false otherwise -

-

- Log progress to the console while building. -

-
- -
- sourcemap -

- --sourcemap (aliases: -sm, sourcemaps) -

-

- Output sourcemaps. -

-
- -
- target -

- --target (aliases: -t, -dev, -prod) default value: development -

-

- Defines the build target. -

-
- -
- vendor-chunk -

- --vendor-chunk (aliases: -vc) default value: true -

-

- Use a separate bundle containing only vendor libraries. -

-
- -
- common-chunk -

- --common-chunk (aliases: -cc) default value: true -

-

- Use a separate bundle containing code used across multiple bundles. -

-
- -
- verbose -

- --verbose (aliases: -v) default value: false -

-

- Adds more details to output logging. -

-
- -
- watch -

- --watch (aliases: -w) -

-

- Run build when files change. -

-
diff --git a/docs/documentation/1-x/generate.md b/docs/documentation/1-x/generate.md deleted file mode 100644 index 74bae41b3a65..000000000000 --- a/docs/documentation/1-x/generate.md +++ /dev/null @@ -1,48 +0,0 @@ - - -# ng generate - -## Overview -`ng generate [name]` generates the specified blueprint - -## Available blueprints: - - [class](1-x/generate/class) - - [component](1-x/generate/component) - - [directive](1-x/generate/directive) - - [enum](1-x/generate/enum) - - [guard](1-x/generate/guard) - - [interface](1-x/generate/interface) - - [module](1-x/generate/module) - - [pipe](1-x/generate/pipe) - - [service](1-x/generate/service) - -## Options -
- dry-run -

- --dry-run (aliases: -d) default value: false -

-

- Run through without making any changes. -

-
- -
- force -

- --force (aliases: -f) default value: false -

-

- Forces overwriting of files. -

-
- -
- app -

- --app -

-

- Specifies app name to use. -

-
diff --git a/docs/documentation/1-x/generate/class.md b/docs/documentation/1-x/generate/class.md deleted file mode 100644 index e1ccd6f5c1b0..000000000000 --- a/docs/documentation/1-x/generate/class.md +++ /dev/null @@ -1,27 +0,0 @@ - - -# ng generate class - -## Overview -`ng generate class [name]` generates a class - -## Options -
- app -

- --app (aliases: -a) default value: 1st app -

-

- Specifies app name to use. -

-
- -
- spec -

- --spec -

-

- Specifies if a spec file is generated. -

-
diff --git a/docs/documentation/1-x/generate/component.md b/docs/documentation/1-x/generate/component.md deleted file mode 100644 index 37b31a077b67..000000000000 --- a/docs/documentation/1-x/generate/component.md +++ /dev/null @@ -1,117 +0,0 @@ - - -# ng generate component - -## Overview -`ng generate component [name]` generates a component - -## Options -
- app -

- --app (aliases: -a) default value: 1st app -

-

- Specifies app name to use. -

-
- -
- change-detection -

- --change-detection (aliases: -c) -

-

- Specifies the change detection strategy. -

-
- -
- flat -

- --flat default value: false -

-

- Flag to indicate if a dir is created. -

-
- -
- export -

- --export default value: false -

-

- Specifies if declaring module exports the component. -

-
- -
- inline-style -

- --inline-style (aliases: -s) default value: false -

-

- Specifies if the style will be in the ts file. -

-
- -
- inline-template -

- --inline-template (aliases: -t) default value: false -

-

- Specifies if the template will be in the ts file. -

-
- -
- module -

- --module (aliases: -m) -

-

- Allows specification of the declaring module's file name (e.g `app.module.ts`). -

-
- -
- prefix -

- --prefix -

-

- Specifies whether to use the prefix. -

-
- -
- skip-import -

- --skip-import default value: false -

-

- Allows for skipping the module import. -

-
- -
- spec -

- --spec -

-

- Specifies if a spec file is generated. -

-
- -
- view-encapsulation -

- --view-encapsulation (aliases: -v) -

-

- Specifies the view encapsulation strategy. -

-
diff --git a/docs/documentation/1-x/generate/directive.md b/docs/documentation/1-x/generate/directive.md deleted file mode 100644 index 9ed8f37046c7..000000000000 --- a/docs/documentation/1-x/generate/directive.md +++ /dev/null @@ -1,77 +0,0 @@ - - -# ng generate directive - -## Overview -`ng generate directive [name]` generates a directive - -## Options -
- app -

- --app (aliases: -a) default value: 1st app -

-

- Specifies app name to use. -

-
- -
- export -

- --export default value: false -

-

- Specifies if declaring module exports the component. -

-
- -
- flat -

- --flat -

-

- Flag to indicate if a dir is created. -

-
- -
- module -

- --module (aliases: -m) -

-

- Allows specification of the declaring module. -

-
- -
- prefix -

- --prefix -

-

- Specifies whether to use the prefix. -

-
- -
- skip-import -

- --skip-import -

-

- Allows for skipping the module import. -

-
- -
- spec -

- --spec -

-

- Specifies if a spec file is generated. -

-
diff --git a/docs/documentation/1-x/generate/enum.md b/docs/documentation/1-x/generate/enum.md deleted file mode 100644 index c7b4b66ff19f..000000000000 --- a/docs/documentation/1-x/generate/enum.md +++ /dev/null @@ -1,17 +0,0 @@ - - -# ng generate enum - -## Overview -`ng generate enum [name]` generates an enumeration - -## Options -
- app -

- --app (aliases: -a) default value: 1st app -

-

- Specifies app name to use. -

-
diff --git a/docs/documentation/1-x/generate/guard.md b/docs/documentation/1-x/generate/guard.md deleted file mode 100644 index 3435dd172f66..000000000000 --- a/docs/documentation/1-x/generate/guard.md +++ /dev/null @@ -1,45 +0,0 @@ -# ng generate guard - -## Overview -`ng generate guard [name]` generates a guard - -## Options -
- app -

- --app (aliases: -a) default value: 1st app -

-

- Specifies app name to use. -

-
- -
- flat -

- --flat -

-

- Indicate if a dir is created. -

-
- -
- module -

- --module (aliases: -m) -

-

- Specifies where the guard should be provided. -

-
- -
- spec -

- --spec -

-

- Specifies if a spec file is generated. -

-
diff --git a/docs/documentation/1-x/generate/interface.md b/docs/documentation/1-x/generate/interface.md deleted file mode 100644 index 8aa09de4ca76..000000000000 --- a/docs/documentation/1-x/generate/interface.md +++ /dev/null @@ -1,24 +0,0 @@ - - -# ng generate interface - -## Overview -`ng generate interface [name] ` generates an interface - -## Options -
- app -

- --app (aliases: -a) default value: 1st app -

-

- Specifies app name to use. -

-
- -
- type -

- Optional String to specify the type of interface. -

-
diff --git a/docs/documentation/1-x/generate/module.md b/docs/documentation/1-x/generate/module.md deleted file mode 100644 index 55559f122406..000000000000 --- a/docs/documentation/1-x/generate/module.md +++ /dev/null @@ -1,59 +0,0 @@ - - -# ng generate module - -## Overview -`ng generate module [name]` generates an NgModule - -## Options -
- app -

- --app (aliases: -a) default value: 1st app -

-

- Specifies app name to use. -

-
- -
- flat -

- --flat -

-

- Flag to indicate if a dir is created. -

-
- -
- module -

- --module (aliases: -m) -

-

- Specifies where the module should be imported. -

-
- -
- spec -

- --spec -

-

- Specifies if a spec file is generated. -

-
- -
- routing -

- --routing -

-

- Specifies if a routing module file should be generated. -

-
- - diff --git a/docs/documentation/1-x/generate/pipe.md b/docs/documentation/1-x/generate/pipe.md deleted file mode 100644 index b8ee09497f3a..000000000000 --- a/docs/documentation/1-x/generate/pipe.md +++ /dev/null @@ -1,67 +0,0 @@ - - -# ng generate pipe - -## Overview -`ng generate pipe [name]` generates a pipe - -## Options -
- app -

- --app (aliases: -a) default value: 1st app -

-

- Specifies app name to use. -

-
- -
- export -

- --export -

-

- Specifies if declaring module exports the pipe. -

-
- -
- flat -

- --flat -

-

- Flag to indicate if a dir is created. -

-
- -
- module -

- --module (aliases: -m) -

-

- Allows specification of the declaring module. -

-
- -
- skip-import -

- --skip-import -

-

- Allows for skipping the module import. -

-
- -
- spec -

- --spec -

-

- Specifies if a spec file is generated. -

-
diff --git a/docs/documentation/1-x/generate/service.md b/docs/documentation/1-x/generate/service.md deleted file mode 100644 index eb94f5c5b9bc..000000000000 --- a/docs/documentation/1-x/generate/service.md +++ /dev/null @@ -1,47 +0,0 @@ - - -# ng generate service - -## Overview -`ng generate service [name]` generates a service - -## Options -
- app -

- --app (aliases: -a) default value: 1st app -

-

- Specifies app name to use. -

-
- -
- flat -

- --flat -

-

- Flag to indicate if a dir is created. -

-
- -
- module -

- --module (aliases: -m) -

-

- Specifies where the service should be provided. -

-
- -
- spec -

- --spec -

-

- Specifies if a spec file is generated. -

-
diff --git a/docs/documentation/1-x/home.md b/docs/documentation/1-x/home.md deleted file mode 100644 index ca01215d4f02..000000000000 --- a/docs/documentation/1-x/home.md +++ /dev/null @@ -1,66 +0,0 @@ - - -# Angular CLI - -NOTE: this documentation is for Angular CLI 1.x. For Angular CLI 6 go [here](home) instead. - -### Overview -The Angular CLI is a tool to initialize, develop, scaffold and maintain [Angular](https://angular.io) applications - -### Getting Started -To install the Angular CLI: -``` -npm install -g @angular/cli -``` - -Generating and serving an Angular project via a development server -[Create](1-x/new) and [run](1-x/serve) a new project: -``` -ng new my-project -cd my-project -ng serve -``` -Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files. - -### Bundling - -All builds make use of bundling, and using the `--prod` flag in `ng build --prod` -or `ng serve --prod` will also make use of uglifying and tree-shaking functionality. - -### Running unit tests - -```bash -ng test -``` - -Tests will execute after a build is executed via [Karma](http://karma-runner.github.io/0.13/index.html), and it will automatically watch your files for changes. You can run tests a single time via `--watch=false` or `--single-run`. - -### Running end-to-end tests - -```bash -ng e2e -``` - -Before running the tests make sure you are serving the app via `ng serve`. -End-to-end tests are run via [Protractor](https://angular.github.io/protractor/). - -### Additional Commands -* [ng new](1-x/new) -* [ng serve](1-x/serve) -* [ng generate](1-x/generate) -* [ng lint](1-x/lint) -* [ng test](1-x/test) -* [ng e2e](1-x/e2e) -* [ng build](1-x/build) -* [ng get/ng set](1-x/config) -* [ng doc](1-x/doc) -* [ng eject](1-x/eject) -* [ng xi18n](1-x/xi18n) -* [ng update](1-x/update) - -## Angular CLI Config Schema -* [Config Schema](1-x/angular-cli) - -### Additional Information -There are several [stories](1-x/stories) which will walk you through setting up -additional aspects of Angular applications. diff --git a/docs/documentation/1-x/lint.md b/docs/documentation/1-x/lint.md deleted file mode 100644 index 46a005c8741e..000000000000 --- a/docs/documentation/1-x/lint.md +++ /dev/null @@ -1,47 +0,0 @@ - - -# ng lint - -## Overview -`ng lint` will lint you app code using tslint. - -## Options -
- fix -

- --fix default value: false -

-

- Fixes linting errors (may overwrite linted files). -

-
- -
- force -

- --force default value: false -

-

- Succeeds even if there was linting errors. -

-
- -
- type-check -

- --type-check default value: false -

-

- Controls the type check for linting. -

-
- -
- format -

- --format (aliases: -t) default value: prose -

-

- Output format (prose, json, stylish, verbose, pmd, msbuild, checkstyle, vso, fileslist, codeFrame). -

-
diff --git a/docs/documentation/1-x/new.md b/docs/documentation/1-x/new.md deleted file mode 100644 index 2dedb230af07..000000000000 --- a/docs/documentation/1-x/new.md +++ /dev/null @@ -1,168 +0,0 @@ - - -# ng new - -## Overview -`ng new [name]` creates a new angular application. - -Default applications are created in a directory of the same name, with an initialized Angular application. - -## Options -
- directory -

- --directory (alias: -d) default value: dir -

-

- The directory name to create the app in. -

-
- -
- dry-run -

- --dry-run (alias: -d) default value: false -

-

- Run through without making any changes. Will list all files that would have been created when running ng new. -

-
- -
- inline-style -

- --inline-style (alias: -s) default value: false -

-

- Should have an inline style. -

-
- -
- inline-template -

- --inline-template (alias: -t) default value: false -

-

- Should have an inline template. -

-
- -
- minimal -

- --minimal default value: false -

-

- Should create a minimal app. -

-
- -
- prefix -

- --prefix (alias: -p) default value: app -

-

- The prefix to use for all component selectors. -

-

- You can later change the value in .angular-cli.json (apps[0].prefix). -

-
- -
- routing -

- --routing default value: false -

-

- Generate a routing module. -

-
- -
- skip-commit -

- --skip-commit (alias: -sc) default value: false -

-

- Skip committing the first commit to git. -

-
- -
- skip-git -

- --skip-git (alias: -g) default value: false -

-

- Skip initializing a git repository. -

-
- -
- skip-install -

- --skip-install (alias: -si) default value: false -

-

- Skip installing packages. -

-
- -
- skip-tests -

- --skip-tests (aliases: -S) default value: false -

-

- Skip creating spec files. -

-

- Skip including e2e functionality. -

-
- -
- source-dir -

- --source-dir (alias: -D) default value: src -

-

- The name of the source directory. -

-

- You can later change the value in .angular-cli.json (apps[0].root). -

-
- -
- style -

- --style default value: css -

-
- The style file default extension. Possible values: -
    -
  • css
  • -
  • scss
  • -
  • less
  • -
  • sass
  • -
  • styl (stylus)
  • -
-
-

- You can later change the value in .angular-cli.json (defaults.styleExt). -

-
- -
- verbose -

- --verbose (alias: -v) default value: false -

-

- Adds more details to output logging. -

-
diff --git a/docs/documentation/1-x/serve.md b/docs/documentation/1-x/serve.md deleted file mode 100644 index 2a441a2dc59c..000000000000 --- a/docs/documentation/1-x/serve.md +++ /dev/null @@ -1,316 +0,0 @@ - - -# ng serve - -## Overview -`ng serve` builds the application and starts a web server. - -All the build Options are available in serve, below are the additional options. - -## Options -
- host -

- --host (aliases: -H) default value: localhost -

-

- Listens only on localhost by default. -

-
- -
- hmr -

- --hmr default value: false -

-

- Enable hot module replacement. -

-
- -
- live-reload -

- --live-reload (aliases: -lr) default value: true -

-

- Whether to reload the page on change, using live-reload. -

-
- -
- public-host -

- --public-host (aliases: --live-reload-client) -

-

- Specify the URL that the browser client will use. -

-
- -
- disable-host-check -

- --disable-host-check default value: false -

-

- Don't verify connected clients are part of allowed hosts. -

-
- -
- open -

- --open (aliases: -o) default value: false -

-

- Opens the url in default browser. -

-
- -
- port -

- --port (aliases: -p) default value: 4200 -

-

- Port to listen to for serving. --port 0 will get a free port -

-
- -
- ssl -

- --ssl -

-

- Serve using HTTPS. -

-
- -
- ssl-cert -

- --ssl-cert (aliases: -) default value: -

-

- SSL certificate to use for serving HTTPS. -

-
- -
- ssl-key -

- --ssl-key -

-

- SSL key to use for serving HTTPS. -

-
- -
- aot -

- --aot -

-

- Build using Ahead of Time compilation. -

-
- -
- base-href -

- --base-href (aliases: -bh) -

-

- Base url for the application being built. -

-
- -
- deploy-url -

- --deploy-url (aliases: -d) -

-

- URL where files will be deployed. -

-
- -
- environment -

- --environment (aliases: -e) -

-

- Defines the build environment. -

-
- -
- extract-css -

- --extract-css (aliases: -ec) -

-

- Extract css from global styles onto css files instead of js ones. -

-
- -
- i18n-file -

- --i18n-file -

-

- Localization file to use for i18n. -

-
- -
- i18n-format -

- --i18n-format -

-

- Format of the localization file specified with --i18n-file. -

-
- -
- locale -

- --locale -

-

- Locale to use for i18n. -

-
- -
- missing-translation -

- --missing-translation -

-

- How to handle missing translations for i18n. -

-

- Values: error, warning, ignore -

-
- -
- output-hashing -

- --output-hashing (aliases: -oh) default value: -

-

- Define the output filename cache-busting hashing mode. Possible values: none, all, media, bundles -

-
- -
- output-path -

- --output-path (aliases: -op) default value: -

-

- Path where output will be placed. -

-
- -
- poll -

- --poll -

-

- Enable and define the file watching poll time period (milliseconds) . -

-
- -
- progress -

- --progress (aliases: -pr) default value: true inside TTY, false otherwise -

-

- Log progress to the console while building. -

-
- -
- proxy-config -

- --proxy-config (aliases: -pc) -

-

- Use a proxy configuration file to send some requests to a backend server rather than the webpack dev server. -

-
- -
- sourcemap -

- --sourcemap (aliases: -sm, sourcemaps) -

-

- Output sourcemaps. -

-
- -
- target -

- --target (aliases: -t, -dev, -prod) default value: development -

-

- Defines the build target. -

-
- -
- vendor-chunk -

- --vendor-chunk (aliases: -vc) default value: true -

-

- Use a separate bundle containing only vendor libraries. -

-
- -
- common-chunk -

- --common-chunk (aliases: -cc) default value: true -

-

- Use a separate bundle containing code used across multiple bundles. -

-
- -
- verbose -

- --verbose (aliases: -v) default value: false -

-

- Adds more details to output logging. -

-
- -
- watch -

- --watch (aliases: -w) -

-

- Run build when files change. -

-
- - -## Note -When running `ng serve`, the compiled output is served from memory, not from disk. This means that the application being served is not located on disk in the `dist` folder. diff --git a/docs/documentation/1-x/stories.md b/docs/documentation/1-x/stories.md deleted file mode 100644 index d602d59e328c..000000000000 --- a/docs/documentation/1-x/stories.md +++ /dev/null @@ -1,34 +0,0 @@ - - -# Stories describing how to do more with the CLI - - - [1.0 Update](1-x/stories/1.0-update) - - [Asset Configuration](1-x/stories/asset-configuration) - - [Autocompletion](1-x/stories/autocompletion) - - [Configure Hot Module Replacement](1-x/stories/configure-hmr) - - [CSS Preprocessors](1-x/stories/css-preprocessors) - - [Global Lib](1-x/stories/global-lib) - - [Global Scripts](1-x/stories/global-scripts) - - [Global Styles](1-x/stories/global-styles) - - [Angular Flex Layout](1-x/stories/include-angular-flex) - - [Angular Material](1-x/stories/include-angular-material) - - [AngularFire](1-x/stories/include-angularfire) - - [Bootstrap](1-x/stories/include-bootstrap) - - [Budgets](1-x/stories/budgets) - - [Font Awesome](1-x/stories/include-font-awesome) - - [Moving Into the CLI](1-x/stories/moving-into-the-cli) - - [Moving Out of the CLI](1-x/stories/moving-out-of-the-cli) - - [Proxy](1-x/stories/proxy) - - [Routing](1-x/stories/routing) - - [3rd Party Lib](1-x/stories/third-party-lib) - - [Corporate Proxy](1-x/stories/using-corporate-proxy) - - [Internationalization (i18n)](1-x/stories/internationalization) - - [Serve from Disk](1-x/stories/disk-serve) - - [Code Coverage](1-x/stories/code-coverage) - - [Application Environments](1-x/stories/application-environments) - - [Autoprefixer Configuration](1-x/stories/autoprefixer) - - [Deploy to GitHub Pages](1-x/stories/github-pages) - - [Linked Library](1-x/stories/linked-library) - - [Multiple apps](1-x/stories/multiple-apps) - - [Continuous Integration](1-x/stories/continuous-integration) - - [Universal Rendering](1-x/stories/universal-rendering) diff --git a/docs/documentation/1-x/stories/1.0-update.md b/docs/documentation/1-x/stories/1.0-update.md deleted file mode 100644 index 9a32e4555ab9..000000000000 --- a/docs/documentation/1-x/stories/1.0-update.md +++ /dev/null @@ -1,503 +0,0 @@ -# Angular CLI migration guide - -In this migration guide we'll be looking at some of the major changes to CLI projects in the -last two months. - -Most of these changes were not breaking changes and your project should work fine without them. - -But if you've been waiting for the perfect time to update, this is it! -Along with major rebuild speed increases, we've been busy adding a lot of features. - -Documentation has also completely moved to [the wiki](https://github.com/angular/angular-cli/wiki). -The new [Stories](https://github.com/angular/angular-cli/wiki/stories) section covers common usage -scenarios, so be sure to have a look! - -Below are the changes between a project generated two months ago, with `1.0.0-beta.24` and -a `1.0.0` project. -If you kept your project up to date you might have a lot of these already. - -You can find more details about changes between versions in [the releases tab on GitHub](https://github.com/angular/angular-cli/releases). - -If you prefer, you can also generate a new project in a separate folder using - `ng new upgrade-project --skip-install` and compare the differences. - -## @angular/cli - -Angular CLI can now be found on NPM under `@angular/cli` instead of `angular-cli`, and upgrading is a simple 3 step process: - -1. Uninstall old version -2. Update node/npm if necessary -3. Install new version - -### 1. Uninstall old version - -If you're using Angular CLI `beta.28` or less, you need to uninstall the `angular-cli` packages: -```bash -npm uninstall -g angular-cli # Remove global package -npm uninstall --save-dev angular-cli # Remove from package.json -``` - -Otherwise, uninstall the `@angular/cli` packages: -```bash -npm uninstall -g @angular/cli # Remove global package -npm uninstall --save-dev @angular/cli # Remove from package.json -``` - -Also purge the cache and local packages: -``` -rm -rf node_modules dist # Use rmdir on Windows -npm cache clean -``` - -At this point, you should not have Angular CLI on your system anymore. If invoking Angular CLI at the commandline reveals that it still exists on your system, you will have to manually remove it. See _Manually removing residual Angular CLI_. - -### 2. Update node/npm if necessary - -Angular CLI now has a minimum requirement of Node 6.9.0 or higher, together with NPM 3 or higher. - -If your Node or NPM versions do not meet these requirements, please refer to [the documentation](https://docs.npmjs.com/getting-started/installing-node) on how to upgrade. - -### 3. Install the new version - -To update Angular CLI to a new version, you must update both the global package and your project's local package: - -```bash -npm install -g @angular/cli@latest # Global package -npm install --save-dev @angular/cli@latest # Local package -npm install # Restore removed dependencies -``` - -### Manually removing residual Angular CLI - -If you accidentally updated NPM before removing the old Angular CLI, you may find that uninstalling the package using `npm uninstall` is proving fruitless. This _could_ be because the global install (and uninstall) path changed between versions of npm from `/usr/local/lib` to `/usr/lib`, and hence, no longer searches through the old directory. In this case you'll have to remove it manually: - -`rm -rf /usr/local/lib/node_modules/@angular/cli` - -If the old Angular CLI package _still_ persists, you'll need to research how to remove it before proceeding with the upgrade. - -## .angular-cli.json - -`angular-cli.json` is now `.angular-cli.json`, but we still accept the old config file name. - -A few new properties have changed in it: - -### Schema - -Add the `$schema` property above project for handy IDE support on your config file: - -``` -"$schema": "./node_modules/@angular/cli/lib/config/schema.json", -``` - -### Polyfills - -There is now a dedicated entry for polyfills ([#3812](https://github.com/angular/angular-cli/pull/3812)) -inside `apps[0].polyfills`, between `main` and `test`: - -``` -"main": "main.ts", -"polyfills": "polyfills.ts", -"test": "test.ts", -``` - -Add it and remove `import './polyfills.ts';` from `src/main.ts` and `src/test.ts`. - -We also added a lot of descriptive comments to the existing `src/polyfills.ts` file, explaining -which polyfills are needed for what browsers. -Be sure to check it out in a new project! - -### Environments - -A new `environmentSource` entry ([#4705](https://github.com/angular/angular-cli/pull/4705)) -replaces the previous source entry inside environments. - -To migrate angular-cli.json follow the example below: - -Before: -``` -"environments": { - "source": "environments/environment.ts", - "dev": "environments/environment.ts", - "prod": "environments/environment.prod.ts" -} -``` - -After: - -``` -"environmentSource": "environments/environment.ts", -"environments": { - "dev": "environments/environment.ts", - "prod": "environments/environment.prod.ts" -} -``` - -### Linting - -The CLI now uses the TSLint API ([#4248](https://github.com/angular/angular-cli/pull/4248)) -to lint several TS projects at once. - -There is a new `lint` entry in `.angular-cli.json` between `e2e` and `test` where all linting -targets are listed: - -``` -"e2e": { - "protractor": { - "config": "./protractor.conf.js" - } -}, -"lint": [ - { - "project": "src/tsconfig.app.json" - }, - { - "project": "src/tsconfig.spec.json" - }, - { - "project": "e2e/tsconfig.e2e.json" - } -], -"test": { - "karma": { - "config": "./karma.conf.js" - } -}, -``` - -### Generator defaults - -Now you can list generator defaults per generator ([#4389](https://github.com/angular/angular-cli/pull/4389)) -in `defaults`. - -Instead of: -``` -"defaults": { - "styleExt": "css", - "prefixInterfaces": false, - "inline": { - "style": false, - "template": false - }, - "spec": { - "class": false, - "component": true, - "directive": true, - "module": false, - "pipe": true, - "service": true - } -} -``` - -You can instead list the flags as they appear on [the generator command](https://github.com/angular/angular-cli/wiki/generate-component): -``` -"defaults": { - "styleExt": "css", - "component": { - "inlineTemplate": false, - "spec": true - } -} -``` - -## One tsconfig per app - -CLI projects now use one tsconfig per app ([#4924](https://github.com/angular/angular-cli/pull/4924)). - -- `src/tsconfig.app.json`: configuration for the Angular app. -``` -{ - "compilerOptions": { - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "target": "es5", - "lib": [ - "es2017", - "dom" - ], - "outDir": "../out-tsc/app", - "module": "es2015", - "baseUrl": "", - "types": [] - }, - "exclude": [ - "test.ts", - "**/*.spec.ts" - ] -} -``` -- `src/tsconfig.spec.json`: configuration for the unit tests. -``` -{ - "compilerOptions": { - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "lib": [ - "es2017", - "dom" - ], - "outDir": "../out-tsc/spec", - "module": "commonjs", - "target": "es5", - "baseUrl": "", - "types": [ - "jasmine", - "node" - ] - }, - "files": [ - "test.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] -} - -``` -- `e2e/tsconfig.e2e.json`: configuration for the e2e tests. -``` -{ - "compilerOptions": { - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "lib": [ - "es2017" - ], - "outDir": "../out-tsc/e2e", - "module": "commonjs", - "target": "es5", - "types":[ - "jasmine", - "node" - ] - } -} - -``` - -There is an additional root-level `tsconfig.json` that is used for IDE integration. -``` -{ - "compileOnSave": false, - "compilerOptions": { - "outDir": "./dist/out-tsc", - "baseUrl": "src", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "target": "es5", - "typeRoots": [ - "node_modules/@types" - ], - "lib": [ - "es2017", - "dom" - ] - } -} -``` - -You can delete `e2e/tsconfig.json` and `src/tsconfig.json` after adding these. - -Also update `.angular-cli.json` to use them inside `apps[0]`: - -``` -"tsconfig": "tsconfig.app.json", -"testTsconfig": "tsconfig.spec.json", -``` - -Then update `protractor.conf.js` to use the e2e config as well: -``` -beforeLaunch: function() { - require('ts-node').register({ - project: 'e2e/tsconfig.e2e.json' - }); -}, -``` - -These configs have an `types` array where you should add any package you install via `@types/*`. -This array helps keep typings isolated to the apps that really need them and avoid problems with -duplicate typings. - -For instance, the unit test `tsconfig` has `jasmine` and `node`, which correspond to -`@types/jasmine` and `@types/node`. -Add any typings you've installed to the appropriate `tsconfig` as well. - -## typings.d.ts - -There's a new `src/typings.d.ts` file that serves two purposes: -- provides a centralized place for users to add their own custom typings -- makes it easy to use components that use `module.id`, present in the documentation and in snippets - -``` -/* SystemJS module definition */ -declare var module: NodeModule; -interface NodeModule { - id: string; -} -``` - -## package.json - -We've updated a lot of packages over the last months in order to keep projects up to date. - -Additions or removals are found in bold below. - -Packages in `dependencies`: -- `@angular/*` packages now have a `^4.0.0` minimum -- `core-js` remains unchanged at `^2.4.1` -- `rxjs` was updated to `^5.1.0` -- `ts-helpers` was **removed** -- `zone.js` was updated to `^0.8.4` - -Packages in `devDependencies`: -- `@angular/cli` at `1.0.0` replaces `angular-cli` -- `@angular/compiler-cli` is also at `^4.0.0` -- `@types/jasmine` remains unchanged and pinned at `2.5.38` -- `@types/node` was updated to `~6.0.60` -- `codelyzer` was updated to `~2.0.0` -- `jasmine-core` was updated to `~2.5.2` -- `jasmine-spec-reporter` was updated to `~3.2.0` -- `karma` was updated to `~1.4.1` -- `karma-chrome-launcher` was updated to `~2.0.0` -- `karma-cli` was updated to `~1.0.1` -- `karma-jasmine` was updated to `~1.1.0` -- `karma-jasmine-html-reporter` was **added** at `^0.2.2` -- `karma-coverage-istanbul-reporter` was **added** at `^0.2.0`, replacing `karma-remap-istanbul` -- `karma-remap-istanbul` was **removed** -- `protractor` was updated to `~5.1.0` -- `ts-node` was updated to `~2.0.0` -- `tslint` was updated to `~4.5.0` -- `typescript` was updated to `~2.1.0` - -See the [karma](1-x/#karma.conf.js) and [protractor](1-x/#protractor.conf.js) sections below for more -information on changed packages. - -The [Linting rules](1-x/#Linting rules) section contains a list of rules that changed due to updates. - -We also updated the scripts section to make it more simple: - -``` -"scripts": { - "ng": "ng", - "start": "ng serve", - "build": "ng build", - "test": "ng test", - "lint": "ng lint", - "e2e": "ng e2e" -}, -``` - -## karma.conf.js - -Karma configuration suffered some changes to improve the code-coverage functionality, -use the new `@angular/cli` package, and the new HTML reporter. - -In the `frameworks` array update the CLI package to `@angular/cli`. - -In the `plugins` array: -- add `require('karma-jasmine-html-reporter')` and `require('karma-coverage-istanbul-reporter')` -- remove `require('karma-remap-istanbul')` -- update the CLI plugin to `require('@angular/cli/plugins/karma')` - -Add a new `client` option just above `patterns`: -``` -client:{ - clearContext: false // leave Jasmine Spec Runner output visible in browser -}, -files: [ -``` - -Change the preprocessor to use the new CLI package: -``` -preprocessors: { - './src/test.ts': ['@angular/cli'] -}, -``` - -Replace `remapIstanbulReporter` with `coverageIstanbulReporter`: -``` -coverageIstanbulReporter: { - reports: [ 'html', 'lcovonly' ], - fixWebpackSourcePaths: true -}, -``` - -Remove the config entry from `angularCli`: -``` -angularCli: { - environment: 'dev' -}, -``` - -Update the reporters to use `coverage-istanbul` instead of `karma-remap-istanbul`, and -add `kjhtml` (short for karma-jasmine-html-reporter): -``` -reporters: config.angularCli && config.angularCli.codeCoverage - ? ['progress', 'coverage-istanbul'] - : ['progress', 'kjhtml'], -``` - -## protractor.conf.js - -Protractor was updated to the new 5.x major version, but you shouldn't need to change much -to take advantage of all its new features. - -Replace the spec reporter import from: -``` -var SpecReporter = require('jasmine-spec-reporter'); -``` -to -``` -const { SpecReporter } = require('jasmine-spec-reporter'); -``` - -Remove `useAllAngular2AppRoots: true`. - -Update `beforeLaunch` as described in [One tsconfig per app](1-x/#one-tsconfig-per-app): -``` -beforeLaunch: function() { - require('ts-node').register({ - project: 'e2e/tsconfig.e2e.json' - }); -}, -``` - -Update `onPrepare`: -``` -onPrepare: function() { - jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); -} -``` - -## Linting rules - -The updated versions of `tslint` and `codelyzer` contain a few rule changes that you should -apply to your `tslint.json`: - -Add these new rules: -``` -"callable-types": true, -"import-blacklist": [true, "rxjs"], -"import-spacing": true, -"interface-over-type-literal": true, -"no-empty-interface": true, -"no-string-throw": true, -"prefer-const": true, -"typeof-compare": true, -"unified-signatures": true, -``` - -Update `no-inferrable-types` to `"no-inferrable-types": [true, "ignore-params"]`. diff --git a/docs/documentation/1-x/stories/application-environments.md b/docs/documentation/1-x/stories/application-environments.md deleted file mode 100644 index d3c4b18d054c..000000000000 --- a/docs/documentation/1-x/stories/application-environments.md +++ /dev/null @@ -1,122 +0,0 @@ -# Application Environments - -## Configuring available environments - -`.angular-cli.json` contains an **environments** section. By default, this looks like: - -``` json -"environments": { - "dev": "environments/environment.ts", - "prod": "environments/environment.prod.ts" -} -``` - -You can add additional environments as required. To add a **staging** environment, your configuration would look like: - -``` json -"environments": { - "dev": "environments/environment.ts", - "staging": "environments/environment.staging.ts", - "prod": "environments/environment.prod.ts" -} -``` - -## Adding environment-specific files - -The environment-specific files are set out as shown below: - -``` -└── src - └── environments - ├── environment.prod.ts - └── environment.ts -``` - -If you wanted to add another environment for **staging**, your file structure would become: - -``` -└── src - └── environments - ├── environment.prod.ts - ├── environment.staging.ts - └── environment.ts -``` - -## Amending environment-specific files - -`environment.ts` contains the default settings. If you take a look at this file, it should look like: - -``` TypeScript -export const environment = { - production: false -}; -``` - -If you compare this to `environment.prod.ts`, which looks like: - -``` TypeScript -export const environment = { - production: true -}; -``` - -You can add further variables, either as additional properties on the `environment` object, or as separate objects, for example: - -``` TypeScript -export const environment = { - production: false, - apiUrl: 'http://my-api-url/' -}; -``` - -## Using environment-specific variables in your application - -Given the following application structure: - -``` -└── src - └── app - ├── app.component.html - └── app.component.ts - └── environments - ├── environment.prod.ts - ├── environment.staging.ts - └── environment.ts -``` - -Using environment variables inside of `app.component.ts` might look something like this: - -``` TypeScript -import { Component } from '@angular/core'; -import { environment } from './../environments/environment'; - -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] -}) -export class AppComponent { - constructor() { - console.log(environment.production); // Logs false for default environment - } - title = 'app works!'; -} -``` - -## Environment-specific builds - -Running: - -``` -ng build -``` - -Will use the defaults found in `environment.ts` - -Running: - -``` -ng build --env=staging -``` - -Will use the values from `environment.staging.ts` diff --git a/docs/documentation/1-x/stories/asset-configuration.md b/docs/documentation/1-x/stories/asset-configuration.md deleted file mode 100644 index ef910b1d8561..000000000000 --- a/docs/documentation/1-x/stories/asset-configuration.md +++ /dev/null @@ -1,62 +0,0 @@ -# Project assets - -You use the `assets` array in `.angular-cli.json` to list files or folders you want to copy as-is -when building your project. - -By default, the `src/assets/` folder and `src/favicon.ico` are copied over. - -```json -"assets": [ - "assets", - "favicon.ico" -] -``` - -You can also further configure assets to be copied by using objects as configuration. - -The array below does the same as the default one: - -```json -"assets": [ - { "glob": "**/*", "input": "./assets/", "output": "./assets/" }, - { "glob": "favicon.ico", "input": "./", "output": "./" }, -] -``` - -`glob` is the a [node-glob](https://github.com/isaacs/node-glob) using `input` as base directory. -`input` is relative to the project root (`src/` default), while `output` is - relative to `outDir` (`dist` default). - - You can use this extended configuration to copy assets from outside your project. - For instance, you can copy assets from a node package: - - ```json -"assets": [ - { "glob": "**/*", "input": "../node_modules/some-package/images", "output": "./some-package/" }, -] -``` - -The contents of `node_modules/some-package/images/` will be available in `dist/some-package/`. - -## Writing assets outside of `dist/` - -Because of the security implications, the CLI will always refuse to read or write files outside of -the project itself (scoped by `.angular-cli.json`). It is however possible to write assets outside -the `dist/` build output folder during build. - -Because writing files in your project isn't an expected effect of `ng build`, it is disabled by -default on every assets. In order to allow this behaviour, you need to set `allowOutsideOutDir` -to `true` on your asset definition, like so: - -```json -"assets": [ - { - "glob": "**/*", - "input": "./assets/", - "output": "../not-dist/some/folder/", - "allowOutsideOutDir": true - }, -] -``` - -This needs to be set for every assets you want to write outside of your build output directory. diff --git a/docs/documentation/1-x/stories/autocompletion.md b/docs/documentation/1-x/stories/autocompletion.md deleted file mode 100644 index 324d6a2faa58..000000000000 --- a/docs/documentation/1-x/stories/autocompletion.md +++ /dev/null @@ -1,21 +0,0 @@ -# Autocompletion - -To turn on auto completion use the following commands: - -For bash: -```bash -ng completion --bash >> ~/.bashrc -source ~/.bashrc -``` - -For zsh: -```bash -ng completion --zsh >> ~/.zshrc -source ~/.zshrc -``` - -Windows users using gitbash: -```bash -ng completion --bash >> ~/.bash_profile -source ~/.bash_profile -``` \ No newline at end of file diff --git a/docs/documentation/1-x/stories/autoprefixer.md b/docs/documentation/1-x/stories/autoprefixer.md deleted file mode 100644 index 78b55b553b64..000000000000 --- a/docs/documentation/1-x/stories/autoprefixer.md +++ /dev/null @@ -1,43 +0,0 @@ -# Change target browsers for Autoprefixer - -Currently, the CLI uses [Autoprefixer](https://github.com/postcss/autoprefixer) to ensure compatibility -with different browser and browser versions. You may find it necessary to target specific browsers -or exclude certain browser versions from your build. - -Internally, Autoprefixer relies on a library called [Browserslist](https://github.com/ai/browserslist) -to figure out which browsers to support with prefixing. - -There are a few ways to tell Autoprefixer what browsers to target: - -### Add a browserslist property to the `package.json` file -``` -"browserslist": [ - "> 1%", - "last 2 versions" -] -``` - -### Add a new file to the project directory called `.browserslistrc` -``` -### Supported Browsers - -> 1% -last 2 versions -``` - -Autoprefixer will look for the configuration file/property to use when it prefixes your css. -Check out the [browserslist repo](https://github.com/ai/browserslist) for more examples of how to target -specific browsers and versions. - -_Side note:_ -Those who are seeking to produce a [progressive web app](https://developers.google.com/web/progressive-web-apps/) and are using [Lighthouse](https://developers.google.com/web/tools/lighthouse/) to grade the project will -need to add the following browserslist config to their package.json file to eliminate the [old flexbox](https://developers.google.com/web/tools/lighthouse/audits/old-flexbox) prefixes: - -`package.json` config: -``` -"browserslist": [ - "last 2 versions", - "not ie <= 10", - "not ie_mob <= 10" -] -``` diff --git a/docs/documentation/1-x/stories/budgets.md b/docs/documentation/1-x/stories/budgets.md deleted file mode 100644 index e01364f9ce09..000000000000 --- a/docs/documentation/1-x/stories/budgets.md +++ /dev/null @@ -1,62 +0,0 @@ -# Budgets - -As applications grow in functionality, they also grow in size. Budgets is a feature in the -Angular CLI which allows you to set budget thresholds in your configuration to ensure parts -of your application stay within boundries which you set. - -**.angular-cli.json** -``` -{ - ... - apps: [ - { - ... - budgets: [] - } - ] -} -``` - -## Budget Definition - -- type - - The type of budget. - - Possible values: - - bundle - The size of a specific bundle. - - initial - The initial size of the app. - - allScript - The size of all scripts. - - all - The size of the entire app. - - anyScript - The size of any one script. - - any - The size of any file. -- name - - The name of the bundle. - - Required only for type of "bundle" -- baseline - - The baseline size for comparison. -- maximumWarning - - The maximum threshold for warning relative to the baseline. -- maximumError - - The maximum threshold for error relative to the baseline. -- minimumWarning - - The minimum threshold for warning relative to the baseline. -- minimumError - - The minimum threshold for error relative to the baseline. -- warning - - The threshold for warning relative to the baseline (min & max). -- error - - The threshold for error relative to the baseline (min & max). - -## Specifying sizes - -Available formats: - -- `123` - size in bytes -- `123b` - size in bytes -- `123kb` - size in kilobytes -- `123mb` - size in megabytes -- `12%` - percentage - -## NOTES - -All sizes are relative to baseline. -Percentages are not valid for baseline values. diff --git a/docs/documentation/1-x/stories/code-coverage.md b/docs/documentation/1-x/stories/code-coverage.md deleted file mode 100644 index 354063cc1242..000000000000 --- a/docs/documentation/1-x/stories/code-coverage.md +++ /dev/null @@ -1,32 +0,0 @@ -# Code Coverage - -With the Angular CLI we can run unit tests as well as create code coverage reports. Code coverage reports allow us to see any parts of our code base that may not be properly tested by our unit tests. - -To generate a coverage report run the following command in the root of your project - -```bash -ng test --watch=false --code-coverage -``` - -Once the tests complete a new `/coverage` folder will appear in the project. In your Finder or Windows Explorer open the `index.html` file. You should see a report with your source code and code coverage values. - -Using the code coverage percentages we can estimate how much of our code is tested. It is up to your team to determine how much code should be unit tested. - -## Code Coverage Enforcement - -If your team decides on a set minimum amount to be unit tested you can enforce this minimum with the Angular CLI. For example our team would like the code base to have a minimum of 80% code coverage. To enable this open the `karma.conf.js` and add the following in the `coverageIstanbulReporter:` key - -```javascript -coverageIstanbulReporter: { - reports: [ 'html', 'lcovonly' ], - fixWebpackSourcePaths: true, - thresholds: { - statements: 80, - lines: 80, - branches: 80, - functions: 80 - } -} -``` - -The `thresholds` property will enforce a minimum of 80% code coverage when the unit tests are run in the project. \ No newline at end of file diff --git a/docs/documentation/1-x/stories/configure-hmr.md b/docs/documentation/1-x/stories/configure-hmr.md deleted file mode 100644 index daa0c3ca56e6..000000000000 --- a/docs/documentation/1-x/stories/configure-hmr.md +++ /dev/null @@ -1,150 +0,0 @@ -# Configure Hot Module Replacement - -Hot Module Replacement (HMR) is a WebPack feature to update code in a running app without rebuilding it. -This results in faster updates and less full page-reloads. - -You can read more about HMR by visiting [this page](https://webpack.js.org/guides/hot-module-replacement). - -In order to get HMR working with Angular CLI we first need to add a new environment and enable it. - -Next we need to update the bootstrap process of our app to enable the -[@angularclass/hmr](https://github.com/AngularClass/angular-hmr) module. - -### Add environment for HMR - -Create a file called `src/environments/environment.hmr.ts` with the following contents: - -```typescript - -export const environment = { - production: false, - hmr: true -}; -``` - -Update `src/environments/environment.prod.ts` and add the `hmr: false` flag to the environment: - -```typescript -export const environment = { - production: true, - hmr: false -}; -``` - -Update `src/environments/environment.ts` and add the `hmr: false` flag to the environment: - -```typescript -export const environment = { - production: false, - hmr: false -}; -``` - - -Update `.angular-cli.json` by adding the new environment the existing environments object: - -```json -"environmentSource": "environments/environment.ts", -"environments": { - "dev": "environments/environment.ts", - "hmr": "environments/environment.hmr.ts", - "prod": "environments/environment.prod.ts" -}, -``` - -Run `ng serve` with the flag `--hmr -e=hmr` to enable hmr and select the new environment: - -```bash -ng serve --hmr -e=hmr -``` - -Create a shortcut for this by updating `package.json` and adding an entry to the script object: - -```json -"scripts": { - ... - "hmr": "ng serve --hmr -e=hmr" -} -``` - - -### Add dependency for @angularclass/hmr and configure app - -In order to get HMR working we need to install the dependency and configure our app to use it. - - -Install the `@angularclass/hmr` module as a dev-dependency - -```bash -$ npm install --save-dev @angularclass/hmr -``` - - -Create a file called `src/hmr.ts` with the following content: - -```typescript -import { NgModuleRef, ApplicationRef } from '@angular/core'; -import { createNewHosts } from '@angularclass/hmr'; - -export const hmrBootstrap = (module: any, bootstrap: () => Promise>) => { - let ngModule: NgModuleRef; - module.hot.accept(); - bootstrap().then(mod => ngModule = mod); - module.hot.dispose(() => { - const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef); - const elements = appRef.components.map(c => c.location.nativeElement); - const makeVisible = createNewHosts(elements); - ngModule.destroy(); - makeVisible(); - }); -}; -``` - - -Update `src/main.ts` to use the file we just created: - -```typescript -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - -import { AppModule } from './app/app.module'; -import { environment } from './environments/environment'; - -import { hmrBootstrap } from './hmr'; - -if (environment.production) { - enableProdMode(); -} - -const bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule); - -if (environment.hmr) { - if (module[ 'hot' ]) { - hmrBootstrap(module, bootstrap); - } else { - console.error('HMR is not enabled for webpack-dev-server!'); - console.log('Are you using the --hmr flag for ng serve?'); - } -} else { - bootstrap(); -} -``` - - -### Starting the development environment with HMR enabled - -Now that everything is set up we can run the new configuration: - -```bash -$ npm run hmr -``` - -When starting the server Webpack will tell you that it’s enabled: - - - NOTICE Hot Module Replacement (HMR) is enabled for the dev server. - - -Now if you make changes to one of your components they changes should be visible automatically without a complete browser refresh. - - diff --git a/docs/documentation/1-x/stories/continuous-integration.md b/docs/documentation/1-x/stories/continuous-integration.md deleted file mode 100644 index 7c079084114b..000000000000 --- a/docs/documentation/1-x/stories/continuous-integration.md +++ /dev/null @@ -1,155 +0,0 @@ -# Continuous Integration - -One of the best ways to keep your project bug free is through a test suite, but it's easy to forget -to run tests all the time. - -That's where Continuous Integration (CI) servers come in. -You can set up your project repository so that your tests run on every commit and pull request. - -There are paid CI services like [Circle CI](https://circleci.com/) and -[Travis CI](https://travis-ci.com/), and you can also host your own for free using -[Jenkins](https://jenkins.io/) and others. - -Even though Circle CI and Travis CI are paid services, they are provided free for open source -projects. -You can create a public project on GitHub and add these services without paying. - -We're going to see how to update your test configuration to run in CI environments, and how to -set up Circle CI and Travis CI. - - -## Update test configuration - -Even though `ng test` and `ng e2e` already run on your environment, they need to be adjusted to -run in CI environments. - -When using Chrome in CI environments it has to be started without sandboxing. -We can achieve that by editing our test configs. - -In `karma.conf.js`, add a custom launcher called `ChromeNoSandbox` below `browsers`: - -``` -browsers: ['Chrome'], -customLaunchers: { - ChromeNoSandbox: { - base: 'Chrome', - flags: ['--no-sandbox'] - } -}, -``` - -Create a new file in the root of your project called `protractor-ci.conf.js`, that extends -the original `protractor.conf.js`: - -``` -const config = require('./protractor.conf').config; - -config.capabilities = { - browserName: 'chrome', - chromeOptions: { - args: ['--no-sandbox'] - } -}; - -exports.config = config; -``` - -Now you can run the following commands to use the `--no-sandbox` flag: - -``` -ng test --single-run --no-progress --browser=ChromeNoSandbox -ng e2e --no-progress --config=protractor-ci.conf.js -``` - -For CI environments it's also a good idea to disable progress reporting (via `--no-progress`) -to avoid spamming the server log with progress messages. - - -## Using Circle CI - -Create a folder called `.circleci` at the project root, and inside of it create a file called -`config.yml`: - -```yaml -version: 2 -jobs: - build: - working_directory: ~/my-project - docker: - - image: circleci/node:8-browsers - steps: - - checkout - - restore_cache: - key: my-project-{{ .Branch }}-{{ checksum "package.json" }} - - run: npm install - - save_cache: - key: my-project-{{ .Branch }}-{{ checksum "package.json" }} - paths: - - "node_modules" - - run: xvfb-run -a npm run test -- --single-run --no-progress --browser=ChromeNoSandbox - - run: xvfb-run -a npm run e2e -- --no-progress --config=protractor-ci.conf.js - -``` - -We're doing a few things here: - - - - `node_modules` is cached. - - [npm run](https://docs.npmjs.com/cli/run-script) is used to run `ng` because `@angular/cli` is - not installed globally. The double dash (`--`) is needed to pass arguments into the npm script. - - `xvfb-run` is used to run `npm run` to run a command using a virtual screen, which is needed by - Chrome. - -Commit your changes and push them to your repository. - -Next you'll need to [sign up for Circle CI](https://circleci.com/docs/2.0/first-steps/) and -[add your project](https://circleci.com/add-projects). -Your project should start building. - -Be sure to check out the [Circle CI docs](https://circleci.com/docs/2.0/) if you want to know more. - - -## Using Travis CI - -Create a file called `.travis.yml` at the project root: - -```yaml -dist: trusty -sudo: false - -language: node_js -node_js: - - "8" - -addons: - apt: - sources: - - google-chrome - packages: - - google-chrome-stable - -cache: - directories: - - ./node_modules - -install: - - npm install - -script: - # Use Chromium instead of Chrome. - - export CHROME_BIN=chromium-browser - - xvfb-run -a npm run test -- --single-run --no-progress --browser=ChromeNoSandbox - - xvfb-run -a npm run e2e -- --no-progress --config=protractor-ci.conf.js - -``` - -Although the syntax is different, we're mostly doing the same steps as were done in the -Circle CI config. -The only difference is that Travis doesn't come with Chrome, so we use Chromium instead. - -Commit your changes and push them to your repository. - -Next you'll need to [sign up for Travis CI](https://travis-ci.org/auth) and -[add your project](https://travis-ci.org/profile). -You'll need to push a new commit to trigger a build. - -Be sure to check out the [Travis CI docs](https://docs.travis-ci.com/) if you want to know more. diff --git a/docs/documentation/1-x/stories/css-preprocessors.md b/docs/documentation/1-x/stories/css-preprocessors.md deleted file mode 100644 index 58bd0071deab..000000000000 --- a/docs/documentation/1-x/stories/css-preprocessors.md +++ /dev/null @@ -1,34 +0,0 @@ -# CSS Preprocessor integration - -Angular CLI supports all major CSS preprocessors: -- sass/scss ([http://sass-lang.com/](http://sass-lang.com/)) -- less ([http://lesscss.org/](http://lesscss.org/)) -- stylus ([http://stylus-lang.com/](http://stylus-lang.com/)) - -To use these preprocessors simply add the file to your component's `styleUrls`: - -```javascript -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] -}) -export class AppComponent { - title = 'app works!'; -} -``` - -When generating a new project you can also define which extension you want for -style files: - -```bash -ng new sassy-project --style=sass -``` - -Or set the default style on an existing project: - -```bash -ng set defaults.styleExt scss -``` - -Style strings added to the `@Component.styles` array _must be written in CSS_ because the CLI cannot apply a pre-processor to inline styles. \ No newline at end of file diff --git a/docs/documentation/1-x/stories/disk-serve.md b/docs/documentation/1-x/stories/disk-serve.md deleted file mode 100644 index b3f4663b630b..000000000000 --- a/docs/documentation/1-x/stories/disk-serve.md +++ /dev/null @@ -1,23 +0,0 @@ -# Serve from Disk - -The CLI supports running a live browser reload experience to users by running `ng serve`. This will compile the application upon file saves and reload the browser with the newly compiled application. This is done by hosting the application in memory and serving it via [webpack-dev-server](https://webpack.js.org/guides/development/#webpack-dev-server). - -If you wish to get a similar experience with the application output to disk please use the steps below. This practice will allow you to ensure that serving the contents of your `dist` dir will be closer to how your application will behave when it is deployed. - -## Environment Setup -### Install a web server -You will not be using webpack-dev-server, so you will need to install a web server for the browser to request the application. There are many to choose from but a good one to try is [lite-server](https://github.com/johnpapa/lite-server) as it will auto-reload your browser when new files are output. - -## Usage -You will need two terminals to get the live-reload experience. The first will run the build in a watch mode to compile the application to the `dist` folder. The second will run the web server against the `dist` folder. The combination of these two processes will mimic the same behavior of ng serve. - -### 1st terminal - Start the build -```bash -ng build --watch -``` - -### 2nd terminal - Start the web server -```bash -lite-server --baseDir="dist" -``` -When using `lite-server` the default browser will open to the appropriate URL. diff --git a/docs/documentation/1-x/stories/github-pages.md b/docs/documentation/1-x/stories/github-pages.md deleted file mode 100644 index cce3c10a5cb5..000000000000 --- a/docs/documentation/1-x/stories/github-pages.md +++ /dev/null @@ -1,21 +0,0 @@ -# Deploy to GitHub Pages - -A simple way to deploy your Angular app is to use -[GitHub Pages](https://help.github.com/articles/what-is-github-pages/). - -The first step is to [create a GitHub account](https://github.com/join), and then -[create a repository](https://help.github.com/articles/create-a-repo/) for your project. -Make a note of the user name and project name in GitHub. - -Then all you need to do is run `ng build --prod --output-path docs --base-href PROJECT_NAME`, where -`PROJECT_NAME` is the name of your project in GitHub. -Make a copy of `docs/index.html` and name it `docs/404.html`. - -Commit your changes and push. On the GitHub project page, configure it to -[publish from the docs folder](https://help.github.com/articles/configuring-a-publishing-source-for-github-pages/#publishing-your-github-pages-site-from-a-docs-folder-on-your-master-branch). - -And that's all you need to do! Now you can see your page at -`https://USER_NAME.github.io/PROJECT_NAME/`. - -You can also use [angular-cli-ghpages](https://github.com/angular-buch/angular-cli-ghpages), a full -featured package that does this all this for you and has extra functionality. diff --git a/docs/documentation/1-x/stories/global-lib.md b/docs/documentation/1-x/stories/global-lib.md deleted file mode 100644 index 91c621acfd5c..000000000000 --- a/docs/documentation/1-x/stories/global-lib.md +++ /dev/null @@ -1,37 +0,0 @@ -# Global Library Installation - -Some javascript libraries need to be added to the global scope, and loaded as if -they were in a script tag. We can do this using the `apps[0].scripts` and -`apps[0].styles` properties of `.angular-cli.json`. - -As an example, to use [Bootstrap 4](https://getbootstrap.com/docs/4.0/getting-started/introduction/) this is -what you need to do: - -First install Bootstrap from `npm`: - -```bash -npm install jquery --save -npm install popper.js --save -npm install bootstrap@next --save -``` - -Then add the needed script files to `apps[0].scripts`: - -```json -"scripts": [ - "../node_modules/jquery/dist/jquery.slim.js", - "../node_modules/popper.js/dist/umd/popper.js", - "../node_modules/bootstrap/dist/js/bootstrap.js" -], -``` - -Finally add the Bootstrap CSS to the `apps[0].styles` array: -```json -"styles": [ - "../node_modules/bootstrap/dist/css/bootstrap.css", - "styles.css" -], -``` - -Restart `ng serve` if you're running it, and Bootstrap 4 should be working on -your app. diff --git a/docs/documentation/1-x/stories/global-scripts.md b/docs/documentation/1-x/stories/global-scripts.md deleted file mode 100644 index fe12d7473e03..000000000000 --- a/docs/documentation/1-x/stories/global-scripts.md +++ /dev/null @@ -1,56 +0,0 @@ -# Global scripts - -You can add Javascript files to the global scope via the `apps[0].scripts` -property in `.angular-cli.json`. -These will be loaded exactly as if you had added them in a `', + ); + indexFileContent.toContain(' { + describe('Behavior: "index.csr.html"', () => { + beforeEach(async () => { + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('main.server.ts'); + + return JSON.stringify(tsConfig); + }); + }); + + it(`should generate 'index.csr.html' instead of 'index.html' when ssr is enabled.`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + server: 'src/main.server.ts', + ssr: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectDirectory('dist/server').toExist(); + harness.expectFile('dist/browser/index.csr.html').toExist(); + harness.expectFile('dist/browser/index.html').toNotExist(); + }); + + it(`should generate 'index.csr.html' instead of 'index.html' when 'output' is 'index.html' and ssr is enabled.`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + index: { + input: 'src/index.html', + output: 'index.html', + }, + server: 'src/main.server.ts', + ssr: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectDirectory('dist/server').toExist(); + harness.expectFile('dist/browser/index.csr.html').toExist(); + harness.expectFile('dist/browser/index.html').toNotExist(); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/index-preload-hints_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/index-preload-hints_spec.ts new file mode 100644 index 000000000000..7f6b9711790b --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/index-preload-hints_spec.ts @@ -0,0 +1,62 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Preload hints"', () => { + it('should add preload hints for transitive global style imports', async () => { + await harness.writeFile( + 'src/styles.css', + ` + @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap'); + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + harness + .expectFile('dist/browser/index.html') + .content.toContain( + '', + ); + }); + + it('should not add preload hints for ssr files', async () => { + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('main.server.ts'); + + return JSON.stringify(tsConfig); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + server: 'src/main.server.ts', + ssr: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/server/main.server.mjs').toExist(); + + harness + .expectFile('dist/browser/index.csr.html') + .content.not.toMatch(//); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/loader-import-attribute_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/loader-import-attribute_spec.ts new file mode 100644 index 000000000000..2f360047b278 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/loader-import-attribute_spec.ts @@ -0,0 +1,148 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "loader import attribute"', () => { + beforeEach(async () => { + await harness.modifyFile('tsconfig.json', (content) => { + return content.replace('"module": "ES2022"', '"module": "esnext"'); + }); + }); + + it('should inline text content for loader attribute set to "text"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + await harness.writeFile('./src/a.unknown', 'ABC'); + await harness.writeFile( + 'src/main.ts', + '// @ts-expect-error\nimport contents from "./a.unknown" with { loader: "text" };\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.toContain('ABC'); + }); + + it('should inline binary content for loader attribute set to "binary"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + await harness.writeFile('./src/a.unknown', 'ABC'); + await harness.writeFile( + 'src/main.ts', + '// @ts-expect-error\nimport contents from "./a.unknown" with { loader: "binary" };\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + // Should contain the binary encoding used esbuild and not the text content + harness.expectFile('dist/browser/main.js').content.toContain('__toBinary("QUJD")'); + harness.expectFile('dist/browser/main.js').content.not.toContain('ABC'); + }); + + it('should emit an output file for loader attribute set to "file"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + await harness.writeFile('./src/a.unknown', 'ABC'); + await harness.writeFile( + 'src/main.ts', + '// @ts-expect-error\nimport contents from "./a.unknown" with { loader: "file" };\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.toContain('a.unknown'); + harness.expectFile('dist/browser/media/a.unknown').toExist(); + }); + + it('should emit an output file with hashing when enabled for loader attribute set to "file"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + outputHashing: 'media' as any, + }); + + await harness.writeFile('./src/a.unknown', 'ABC'); + await harness.writeFile( + 'src/main.ts', + '// @ts-expect-error\nimport contents from "./a.unknown" with { loader: "file" };\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.toContain('a.unknown'); + expect(harness.hasFileMatch('dist/browser/media', /a-[0-9A-Z]{8}\.unknown$/)).toBeTrue(); + }); + + it('should allow overriding default `.txt` extension behavior', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + await harness.writeFile('./src/a.txt', 'ABC'); + await harness.writeFile( + 'src/main.ts', + '// @ts-expect-error\nimport contents from "./a.txt" with { loader: "file" };\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.toContain('a.txt'); + harness.expectFile('dist/browser/media/a.txt').toExist(); + }); + + it('should allow overriding default `.js` extension behavior', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + await harness.writeFile('./src/a.js', 'ABC'); + await harness.writeFile( + 'src/main.ts', + '// @ts-expect-error\nimport contents from "./a.js" with { loader: "file" };\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.toContain('a.js'); + harness.expectFile('dist/browser/media/a.js').toExist(); + }); + + it('should fail with an error if an invalid loader attribute value is used', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + await harness.writeFile('./src/a.unknown', 'ABC'); + await harness.writeFile( + 'src/main.ts', + '// @ts-expect-error\nimport contents from "./a.unknown" with { loader: "invalid" };\n console.log(contents);', + ); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBe(false); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Unsupported loader import attribute'), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts new file mode 100644 index 000000000000..a48c19fd1baf --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts @@ -0,0 +1,110 @@ +/** + * @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 { concatMap, count, take, timeout } from 'rxjs'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * Maximum time in milliseconds for single build/rebuild + * This accounts for CI variability. + */ +const BUILD_TIMEOUT = 10_000; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Rebuilds when input asset changes"', () => { + beforeEach(async () => { + // Application code is not needed for styles tests + await harness.writeFile('src/main.ts', 'console.log("TEST");'); + await harness.writeFile('public/asset.txt', 'foo'); + }); + + it('emits updated asset', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [ + { + glob: '**/*', + input: 'public', + }, + ], + watch: true, + }); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(BUILD_TIMEOUT), + concatMap(async ({ result }, index) => { + switch (index) { + case 0: + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset.txt').content.toContain('foo'); + + await harness.writeFile('public/asset.txt', 'bar'); + break; + case 1: + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset.txt').content.toContain('bar'); + break; + } + }), + take(2), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(2); + }); + + it('remove deleted asset from output', async () => { + await Promise.all([ + harness.writeFile('public/asset-two.txt', 'bar'), + harness.writeFile('public/asset-one.txt', 'foo'), + ]); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [ + { + glob: '**/*', + input: 'public', + }, + ], + watch: true, + }); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(BUILD_TIMEOUT), + concatMap(async ({ result }, index) => { + switch (index) { + case 0: + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset-one.txt').toExist(); + harness.expectFile('dist/browser/asset-two.txt').toExist(); + + await harness.removeFile('public/asset-two.txt'); + break; + case 1: + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset-one.txt').toExist(); + harness.expectFile('dist/browser/asset-two.txt').toNotExist(); + break; + } + }), + take(2), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(2); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts new file mode 100644 index 000000000000..a252a0580d0b --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts @@ -0,0 +1,78 @@ +/** + * @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 { concatMap, count, take, timeout } from 'rxjs'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * Maximum time in milliseconds for single build/rebuild + * This accounts for CI variability. + */ +export const BUILD_TIMEOUT = 30_000; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Rebuilds when component stylesheets change"', () => { + for (const aot of [true, false]) { + it(`updates component when imported sass changes with ${aot ? 'AOT' : 'JIT'}`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + aot, + }); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace('app.component.css', 'app.component.scss'), + ); + await harness.writeFile('src/app/app.component.scss', "@import 'http://github.com/a';"); + await harness.writeFile('src/app/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + + const buildCount = await harness + .execute() + .pipe( + timeout(30000), + concatMap(async ({ result }, index) => { + expect(result?.success).toBe(true); + + switch (index) { + case 0: + harness.expectFile('dist/browser/main.js').content.toContain('color: aqua'); + harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue'); + + await harness.writeFile( + 'src/app/a.scss', + '$primary: blue;\\nh1 { color: $primary; }', + ); + break; + case 1: + harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/main.js').content.toContain('color: blue'); + + await harness.writeFile( + 'src/app/a.scss', + '$primary: green;\\nh1 { color: $primary; }', + ); + break; + case 2: + harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue'); + harness.expectFile('dist/browser/main.js').content.toContain('color: green'); + + break; + } + }), + take(3), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(3); + }); + } + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts new file mode 100644 index 000000000000..196cbf4e6b5d --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts @@ -0,0 +1,373 @@ +/** + * @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 { concatMap, count, take, timeout } from 'rxjs'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * Maximum time in milliseconds for single build/rebuild + * This accounts for CI variability. + */ +export const BUILD_TIMEOUT = 30_000; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Rebuild Error Detection"', () => { + it('detects template errors with no AOT codegen or TS emit differences', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + const goodDirectiveContents = ` + import { Directive, Input } from '@angular/core'; + @Directive({ selector: 'dir', standalone: false }) + export class Dir { + @Input() foo: number; + } + `; + + const typeErrorText = `Type 'number' is not assignable to type 'string'.`; + + // Create a directive and add to application + await harness.writeFile('src/app/dir.ts', goodDirectiveContents); + await harness.writeFile( + 'src/app/app.module.ts', + ` + import { NgModule } from '@angular/core'; + import { BrowserModule } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + import { Dir } from './dir'; + @NgModule({ + declarations: [ + AppComponent, + Dir, + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + `, + ); + + // Create app component that uses the directive + await harness.writeFile( + 'src/app/app.component.ts', + ` + import { Component } from '@angular/core' + @Component({ + selector: 'app-root', + standalone: false, + template: '', + }) + export class AppComponent { } + `, + ); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(BUILD_TIMEOUT), + concatMap(async ({ result, logs }, index) => { + switch (index) { + case 0: + expect(result?.success).toBeTrue(); + + // Update directive to use a different input type for 'foo' (number -> string) + // Should cause a template error + await harness.writeFile( + 'src/app/dir.ts', + ` + import { Directive, Input } from '@angular/core'; + @Directive({ selector: 'dir', standalone: false }) + export class Dir { + @Input() foo: string; + } + `, + ); + + break; + case 1: + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should persist error in the next rebuild + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + + break; + case 2: + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Revert the directive change that caused the error + // Should remove the error + await harness.writeFile('src/app/dir.ts', goodDirectiveContents); + + break; + case 3: + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should continue showing no error + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + + break; + case 4: + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + break; + } + }), + take(5), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(5); + }); + + it('detects cumulative block syntax errors', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(BUILD_TIMEOUT), + concatMap(async ({ logs }, index) => { + switch (index) { + case 0: + // Add invalid block syntax + await harness.appendToFile('src/app/app.component.html', '@one'); + + break; + case 1: + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@one'), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should persist error in the next rebuild + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + + break; + case 2: + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@one'), + }), + ); + + // Add more invalid block syntax + await harness.appendToFile('src/app/app.component.html', '@two'); + + break; + case 3: + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@one'), + }), + ); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@two'), + }), + ); + + // Add more invalid block syntax + await harness.appendToFile('src/app/app.component.html', '@three'); + + break; + case 4: + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@one'), + }), + ); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@two'), + }), + ); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@three'), + }), + ); + + // Revert the changes that caused the error + // Should remove the error + await harness.writeFile('src/app/app.component.html', '

GOOD

'); + + break; + case 5: + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@one'), + }), + ); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@two'), + }), + ); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining('@three'), + }), + ); + + break; + } + }), + take(6), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(6); + }); + + it('recovers from component stylesheet error', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + aot: false, + }); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(BUILD_TIMEOUT), + concatMap(async ({ result, logs }, index) => { + switch (index) { + case 0: + await harness.writeFile('src/app/app.component.css', 'invalid-css-content'); + + break; + case 1: + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('invalid-css-content'), + }), + ); + + await harness.writeFile('src/app/app.component.css', 'p { color: green }'); + + break; + case 2: + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('invalid-css-content'), + }), + ); + + harness + .expectFile('dist/browser/main.js') + .content.toContain('p {\\n color: green;\\n}'); + + break; + } + }), + take(3), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(3); + }); + + it('recovers from component template error', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(BUILD_TIMEOUT), + concatMap(async ({ result, logs }, index) => { + switch (index) { + case 0: + // Missing ending `>` on the div will cause an error + await harness.appendToFile('src/app/app.component.html', '
Hello, world!({ + message: jasmine.stringMatching('Unexpected character "EOF"'), + }), + ); + + await harness.appendToFile('src/app/app.component.html', '>'); + + break; + case 2: + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Unexpected character "EOF"'), + }), + ); + + harness.expectFile('dist/browser/main.js').content.toContain('Hello, world!'); + + // Make an additional valid change to ensure that rebuilds still trigger + await harness.appendToFile('src/app/app.component.html', '
Guten Tag
'); + + break; + case 3: + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('invalid-css-content'), + }), + ); + + harness.expectFile('dist/browser/main.js').content.toContain('Hello, world!'); + harness.expectFile('dist/browser/main.js').content.toContain('Guten Tag'); + + break; + } + }), + take(4), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(4); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-general_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-general_spec.ts new file mode 100644 index 000000000000..ca88f94e5b63 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-general_spec.ts @@ -0,0 +1,112 @@ +/** + * @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 { concatMap, count, take, timeout } from 'rxjs'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * Maximum time in milliseconds for single build/rebuild + * This accounts for CI variability. + */ +export const BUILD_TIMEOUT = 30_000; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Rebuild updates in general cases"', () => { + it('detects changes after a file was deleted and recreated', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + const fileAContent = ` + console.log('FILE-A'); + export {}; + `; + + // Create a file and add to application + await harness.writeFile('src/app/file-a.ts', fileAContent); + await harness.writeFile( + 'src/app/app.component.ts', + ` + import { Component } from '@angular/core'; + import './file-a'; + @Component({ + selector: 'app-root', + standalone: false, + template: 'App component', + }) + export class AppComponent { } + `, + ); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(BUILD_TIMEOUT), + concatMap(async ({ result, logs }, index) => { + switch (index) { + case 0: + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/main.js').content.toContain('FILE-A'); + + // Delete the imported file + await harness.removeFile('src/app/file-a.ts'); + + break; + case 1: + // Should fail from missing import + expect(result?.success).toBeFalse(); + + // Remove the failing import + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace(`import './file-a';`, ''), + ); + + break; + case 2: + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').content.not.toContain('FILE-A'); + + // Recreate the file and the import + await harness.writeFile('src/app/file-a.ts', fileAContent); + await harness.modifyFile( + 'src/app/app.component.ts', + (content) => `import './file-a';\n` + content, + ); + + break; + case 3: + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').content.toContain('FILE-A'); + + // Change the imported file + await harness.modifyFile('src/app/file-a.ts', (content) => + content.replace('FILE-A', 'FILE-B'), + ); + + break; + case 4: + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').content.toContain('FILE-B'); + + break; + } + }), + take(5), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(5); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts new file mode 100644 index 000000000000..e58b2e031a90 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts @@ -0,0 +1,171 @@ +/** + * @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 { concatMap, count, take, timeout } from 'rxjs'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * Maximum time in milliseconds for single build/rebuild + * This accounts for CI variability. + */ +export const BUILD_TIMEOUT = 30_000; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Rebuilds when global stylesheets change"', () => { + beforeEach(async () => { + // Application code is not needed for styles tests + await harness.writeFile('src/main.ts', 'console.log("TEST");'); + }); + + it('rebuilds Sass stylesheet after error on rebuild from import', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + styles: ['src/styles.scss'], + }); + + await harness.writeFile('src/styles.scss', "@import 'http://github.com/a';"); + await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(30000), + concatMap(async ({ result }, index) => { + switch (index) { + case 0: + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + + await harness.writeFile( + 'src/a.scss', + 'invalid-invalid-invalid\\nh1 { color: $primary; }', + ); + break; + case 1: + expect(result?.success).toBe(false); + + await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); + break; + case 2: + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + + break; + } + }), + take(3), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(3); + }); + + it('rebuilds Sass stylesheet after error on initial build from import', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + styles: ['src/styles.scss'], + }); + + await harness.writeFile('src/styles.scss', "@import 'http://github.com/a';"); + await harness.writeFile('src/a.scss', 'invalid-invalid-invalid\\nh1 { color: $primary; }'); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(30000), + concatMap(async ({ result }, index) => { + switch (index) { + case 0: + expect(result?.success).toBe(false); + + await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + break; + case 1: + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + + await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); + break; + case 2: + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + break; + } + }), + take(3), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(3); + }); + + it('rebuilds dependent Sass stylesheets after error on initial build from import', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + styles: [ + { bundleName: 'styles', input: 'src/styles.scss' }, + { bundleName: 'other', input: 'src/other.scss' }, + ], + }); + + await harness.writeFile('src/styles.scss', "@import 'http://github.com/a';"); + await harness.writeFile('src/other.scss', "@import 'http://github.com/a'; h1 { color: green; }"); + await harness.writeFile('src/a.scss', 'invalid-invalid-invalid\\nh1 { color: $primary; }'); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(30000), + concatMap(async ({ result }, index) => { + switch (index) { + case 0: + expect(result?.success).toBe(false); + + await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + break; + case 1: + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: blue'); + + harness.expectFile('dist/browser/other.css').content.toContain('color: green'); + harness.expectFile('dist/browser/other.css').content.toContain('color: aqua'); + harness.expectFile('dist/browser/other.css').content.not.toContain('color: blue'); + + await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); + break; + case 2: + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/styles.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/styles.css').content.toContain('color: blue'); + + harness.expectFile('dist/browser/other.css').content.toContain('color: green'); + harness.expectFile('dist/browser/other.css').content.not.toContain('color: aqua'); + harness.expectFile('dist/browser/other.css').content.toContain('color: blue'); + break; + } + }), + take(3), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(3); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-index-html_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-index-html_spec.ts new file mode 100644 index 000000000000..df9dbc6f0c93 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-index-html_spec.ts @@ -0,0 +1,71 @@ +/** + * @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 { concatMap, count, take, timeout } from 'rxjs'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * Maximum time in milliseconds for single build/rebuild + * This accounts for CI variability. + */ +export const BUILD_TIMEOUT = 30_000; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Rebuilds when input index HTML changes"', () => { + beforeEach(async () => { + // Application code is not needed for styles tests + await harness.writeFile('src/main.ts', 'console.log("TEST");'); + }); + + it('rebuilds output index HTML', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(30000), + concatMap(async ({ result }, index) => { + switch (index) { + case 0: + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/index.html').content.toContain('charset="utf-8"'); + + await harness.modifyFile('src/index.html', (content) => + content.replace('charset="utf-8"', 'abc'), + ); + break; + case 1: + expect(result?.success).toBe(true); + harness + .expectFile('dist/browser/index.html') + .content.not.toContain('charset="utf-8"'); + + await harness.modifyFile('src/index.html', (content) => + content.replace('abc', 'charset="utf-8"'), + ); + break; + case 2: + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/index.html').content.toContain('charset="utf-8"'); + + break; + } + }), + take(3), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(3); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts new file mode 100644 index 000000000000..421e51f99f5b --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts @@ -0,0 +1,139 @@ +/** + * @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 { concatMap, count, take, timeout } from 'rxjs'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * Maximum time in milliseconds for single build/rebuild + * This accounts for CI variability. + */ +export const BUILD_TIMEOUT = 30_000; + +/** + * A regular expression used to check if a built worker is correctly referenced in application code. + */ +const REFERENCED_WORKER_REGEXP = + /new Worker\(new URL\("worker-[A-Z0-9]{8}\.js", import\.meta\.url\)/; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Rebuilds when Web Worker files change"', () => { + it('Recovers from error when directly referenced worker file is changed', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + const workerCodeFile = ` + console.log('WORKER FILE'); + `; + + const errorText = `Expected ";" but found "~"`; + + // Create a worker file + await harness.writeFile('src/app/worker.ts', workerCodeFile); + + // Create app component that uses the directive + await harness.writeFile( + 'src/app/app.component.ts', + ` + import { Component } from '@angular/core' + @Component({ + selector: 'app-root', + standalone: false, + template: '

Worker Test

', + }) + export class AppComponent { + worker = new Worker(new URL('./worker', import.meta.url), { type: 'module' }); + } + `, + ); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(BUILD_TIMEOUT), + concatMap(async ({ result, logs }, index) => { + switch (index) { + case 0: + expect(result?.success).toBeTrue(); + + // Ensure built worker is referenced in the application code + harness + .expectFile('dist/browser/main.js') + .content.toMatch(REFERENCED_WORKER_REGEXP); + + // Update the worker file to be invalid syntax + await harness.writeFile('src/app/worker.ts', `asd;fj$3~kls;kd^(*fjlk;sdj---flk`); + + break; + case 1: + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(errorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should persist error in the next rebuild + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + + break; + case 2: + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(errorText), + }), + ); + + // Revert the change that caused the error + // Should remove the error + await harness.writeFile('src/app/worker.ts', workerCodeFile); + + break; + case 3: + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(errorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should continue showing no error + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + + break; + case 4: + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(errorText), + }), + ); + + // Ensure built worker is referenced in the application code + harness + .expectFile('dist/browser/main.js') + .content.toMatch(REFERENCED_WORKER_REGEXP); + + break; + } + }), + take(5), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(5); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/stylesheet-url-resolution_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/stylesheet-url-resolution_spec.ts new file mode 100644 index 000000000000..0adc77b5311a --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/stylesheet-url-resolution_spec.ts @@ -0,0 +1,458 @@ +/** + * @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 { buildApplication } from '../../index'; +import { OutputHashing } from '../../schema'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Stylesheet url() Resolution"', () => { + it('should show a note when using tilde prefix in a directly referenced stylesheet', async () => { + await harness.writeFile( + 'src/styles.css', + ` + .a { + background-image: url("http://github.com/~/image.jpg") + } + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBe(false); + + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('You can remove the tilde and'), + }), + ); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Preprocessor stylesheets may not show the exact'), + }), + ); + }); + + it('should show a note when using tilde prefix in an imported CSS stylesheet', async () => { + await harness.writeFile( + 'src/styles.css', + ` + @import "http://github.com/a.css"; + `, + ); + await harness.writeFile( + 'src/a.css', + ` + .a { + background-image: url("http://github.com/~/image.jpg") + } + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBe(false); + + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('You can remove the tilde and'), + }), + ); + }); + + it('should show a note when using tilde prefix in an imported Sass stylesheet', async () => { + await harness.writeFile( + 'src/styles.scss', + ` + @import "http://github.com/a"; + `, + ); + await harness.writeFile( + 'src/a.scss', + ` + .a { + background-image: url("http://github.com/~/image.jpg") + } + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.scss'], + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBe(false); + + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('You can remove the tilde and'), + }), + ); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Preprocessor stylesheets may not show the exact'), + }), + ); + }); + + it('should show a note when using caret prefix in a directly referenced stylesheet', async () => { + await harness.writeFile( + 'src/styles.css', + ` + .a { + background-image: url("http://github.com/^image.jpg") + } + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBe(false); + + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('You can remove the caret and'), + }), + ); + }); + + it('should show a note when using caret prefix in an imported Sass stylesheet', async () => { + await harness.writeFile( + 'src/styles.scss', + ` + @import "http://github.com/a"; + `, + ); + await harness.writeFile( + 'src/a.scss', + ` + .a { + background-image: url("http://github.com/^image.jpg") + } + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.scss'], + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBe(false); + + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('You can remove the caret and'), + }), + ); + }); + + it('should not rebase a URL with a namespaced Sass variable reference that points to an absolute asset', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + @use './b' as named; + .a { + background-image: url(http://github.com/named.$my-var) + } + `, + 'src/theme/b.scss': `@forward './c.scss' show $my-var;`, + 'src/theme/c.scss': `$my-var: "https://example.com/example.png";`, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness + .expectFile('dist/browser/styles.css') + .content.toContain('url(https://example.com/example.png)'); + }); + + it('should not rebase a URL with a Sass variable reference that points to an absolute asset', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + @import 'http://github.com/b'; + .a { + background-image: url(http://github.com/$my-var) + } + `, + 'src/theme/b.scss': `$my-var: "https://example.com/example.png";`, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness + .expectFile('dist/browser/styles.css') + .content.toContain('url(https://example.com/example.png)'); + }); + + it('should rebase a URL with a namespaced Sass variable referencing a local resource', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + @use './b' as named; + .a { + background-image: url(http://github.com/named.$my-var) + } + `, + 'src/theme/b.scss': `@forward './c.scss' show $my-var;`, + 'src/theme/c.scss': `$my-var: "./images/logo.svg";`, + 'src/theme/images/logo.svg': ``, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.None, + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/styles.css').content.toContain(`url("http://github.com/media/logo.svg")`); + harness.expectFile('dist/browser/media/logo.svg').toExist(); + }); + + it('should rebase a URL with a hyphen-namespaced Sass variable referencing a local resource', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + @use './b' as named-hyphen; + .a { + background-image: url(http://github.com/named-hyphen.$my-var) + } + `, + 'src/theme/b.scss': `@forward './c.scss' show $my-var;`, + 'src/theme/c.scss': `$my-var: "./images/logo.svg";`, + 'src/theme/images/logo.svg': ``, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.None, + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/styles.css').content.toContain(`url("http://github.com/media/logo.svg")`); + harness.expectFile('dist/browser/media/logo.svg').toExist(); + }); + + it('should rebase a URL with a underscore-namespaced Sass variable referencing a local resource', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + @use './b' as named_underscore; + .a { + background-image: url(http://github.com/named_underscore.$my-var) + } + `, + 'src/theme/b.scss': `@forward './c.scss' show $my-var;`, + 'src/theme/c.scss': `$my-var: "./images/logo.svg";`, + 'src/theme/images/logo.svg': ``, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.None, + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/styles.css').content.toContain(`url("http://github.com/media/logo.svg")`); + harness.expectFile('dist/browser/media/logo.svg').toExist(); + }); + + it('should rebase a URL with a Sass variable referencing a local resource', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + @import 'http://github.com/b'; + .a { + background-image: url(http://github.com/$my-var) + } + `, + 'src/theme/b.scss': `$my-var: "./images/logo.svg";`, + 'src/theme/images/logo.svg': ``, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.None, + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/styles.css').content.toContain(`url("http://github.com/media/logo.svg")`); + harness.expectFile('dist/browser/media/logo.svg').toExist(); + }); + + it('should rebase a URL with an leading interpolation referencing a local resource', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + @import 'http://github.com/b'; + .a { + background-image: url(#{$my-var}logo.svg) + } + `, + 'src/theme/b.scss': `$my-var: "./images/";`, + 'src/theme/images/logo.svg': ``, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.None, + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/styles.css').content.toContain(`url("http://github.com/media/logo.svg")`); + harness.expectFile('dist/browser/media/logo.svg').toExist(); + }); + + it('should rebase a URL with interpolation using concatenation referencing a local resource', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + @import 'http://github.com/b'; + $extra-var: "2"; + $postfix-var: "xyz"; + .a { + background-image: url("#{$my-var}logo#{$extra-var+ "-" + $postfix-var}.svg") + } + `, + 'src/theme/b.scss': `$my-var: "./images/";`, + 'src/theme/images/logo2-xyz.svg': ``, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.None, + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness + .expectFile('dist/browser/styles.css') + .content.toContain(`url("http://github.com/media/logo2-xyz.svg")`); + harness.expectFile('dist/browser/media/logo2-xyz.svg').toExist(); + }); + + it('should rebase a URL with an non-leading interpolation referencing a local resource', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + @import 'http://github.com/b'; + .a { + background-image: url(http://github.com/#{$my-var}logo.svg) + } + `, + 'src/theme/b.scss': `$my-var: "./images/";`, + 'src/theme/images/logo.svg': ``, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.None, + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/styles.css').content.toContain(`url("http://github.com/media/logo.svg")`); + harness.expectFile('dist/browser/media/logo.svg').toExist(); + }); + + it('should not rebase Sass function definition with name ending in "url"', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + @import 'http://github.com/b'; + .a { + $asset: my-function-url('http://github.com/logo'); + background-image: url(http://github.com/$asset) + } + `, + 'src/theme/b.scss': `@function my-function-url(http://github.com/$name) { @return "./images/" + $name + ".svg"; }`, + 'src/theme/images/logo.svg': ``, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.None, + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/styles.css').content.toContain(`url("http://github.com/media/logo.svg")`); + harness.expectFile('dist/browser/media/logo.svg').toExist(); + }); + + it('should not process a URL that has been marked as external', async () => { + await harness.writeFiles({ + 'src/styles.scss': `@use 'theme/a';`, + 'src/theme/a.scss': ` + .a { + background-image: url("http://github.com/assets/logo.svg") + } + `, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.None, + externalDependencies: ['assets/*'], + styles: ['src/styles.scss'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/styles.css').content.toContain(`url(http://github.com/assets/logo.svg)`); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/stylesheet_autoprefixer_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/stylesheet_autoprefixer_spec.ts new file mode 100644 index 000000000000..41ae225e2d3d --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/stylesheet_autoprefixer_spec.ts @@ -0,0 +1,259 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +const styleBaseContent: Record = Object.freeze({ + 'css': ` + @import url(http://github.com/imported-styles.css); + div { hyphens: none; } + `, +}); + +const styleImportedContent: Record = Object.freeze({ + 'css': 'section { hyphens: none; }', +}); + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Stylesheet autoprefixer"', () => { + for (const ext of ['css'] /* ['css', 'sass', 'scss', 'less'] */) { + it(`should add prefixes for listed browsers in global styles [${ext}]`, async () => { + await harness.writeFile( + '.browserslistrc', + ` + Safari 15.4 + Edge 104 + Firefox 91 + `, + ); + + await harness.writeFiles({ + [`src/styles.${ext}`]: styleBaseContent[ext], + [`src/imported-styles.${ext}`]: styleImportedContent[ext], + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [`src/styles.${ext}`], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness + .expectFile('dist/browser/styles.css') + .content.toMatch(/section\s*{\s*-webkit-hyphens:\s*none;\s*hyphens:\s*none;\s*}/); + harness + .expectFile('dist/browser/styles.css') + .content.toMatch(/div\s*{\s*-webkit-hyphens:\s*none;\s*hyphens:\s*none;\s*}/); + }); + + it(`should not add prefixes if not required by browsers in global styles [${ext}]`, async () => { + await harness.writeFile( + '.browserslistrc', + ` + Edge 110 + `, + ); + + await harness.writeFiles({ + [`src/styles.${ext}`]: styleBaseContent[ext], + [`src/imported-styles.${ext}`]: styleImportedContent[ext], + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [`src/styles.${ext}`], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness + .expectFile('dist/browser/styles.css') + .content.toMatch(/section\s*{\s*hyphens:\s*none;\s*}/); + harness + .expectFile('dist/browser/styles.css') + .content.toMatch(/div\s*{\s*hyphens:\s*none;\s*}/); + }); + + it(`should add prefixes for listed browsers in external component styles [${ext}]`, async () => { + await harness.writeFile( + '.browserslistrc', + ` + Safari 15.4 + Edge 104 + Firefox 91 + `, + ); + + await harness.writeFiles({ + [`src/app/app.component.${ext}`]: styleBaseContent[ext], + [`src/app/imported-styles.${ext}`]: styleImportedContent[ext], + }); + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace('./app.component.css', `./app.component.${ext}`), + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness + .expectFile('dist/browser/main.js') + .content.toMatch(/{\\n\s*-webkit-hyphens:\s*none;\\n\s*hyphens:\s*none;\\n\s*}/); + harness + .expectFile('dist/browser/main.js') + .content.toMatch(/{\\n\s*-webkit-hyphens:\s*none;\\n\s*hyphens:\s*none;\\n\s*}/); + }); + + it(`should not add prefixes if not required by browsers in external component styles [${ext}]`, async () => { + await harness.writeFile( + '.browserslistrc', + ` + Edge 110 + `, + ); + + await harness.writeFiles({ + [`src/app/app.component.${ext}`]: styleBaseContent[ext], + [`src/app/imported-styles.${ext}`]: styleImportedContent[ext], + }); + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace('./app.component.css', `./app.component.${ext}`), + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness + .expectFile('dist/browser/main.js') + .content.toMatch(/{\\n\s*hyphens:\s*none;\\n\s*}/); + harness + .expectFile('dist/browser/main.js') + .content.toMatch(/{\\n\s*hyphens:\s*none;\\n\s*}/); + }); + } + + it('should add prefixes for listed browsers in inline component styles', async () => { + await harness.writeFile( + '.browserslistrc', + ` + Safari 15.4 + Edge 104 + Firefox 91 + `, + ); + + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content + .replace('styleUrls', 'styles') + .replace('./app.component.css', 'div { hyphens: none; }'); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness + .expectFile('dist/browser/main.js') + // div[_ngcontent-%COMP%] {\n -webkit-hyphens: none;\n hyphens: none;\n}\n + .content.toMatch(/{\\n\s*-webkit-hyphens:\s*none;\\n\s*hyphens:\s*none;\\n\s*}/); + }); + + it('should not add prefixes if not required by browsers in inline component styles', async () => { + await harness.writeFile( + '.browserslistrc', + ` + Edge 110 + `, + ); + + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content + .replace('styleUrls', 'styles') + .replace('./app.component.css', 'div { hyphens: none; }'); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').content.toMatch(/{\\n\s*hyphens:\s*none;\\n\s*}/); + }); + + it('should add prefixes for listed browsers in inline template styles', async () => { + await harness.writeFile( + '.browserslistrc', + ` + Safari 15.4 + Edge 104 + Firefox 91 + `, + ); + + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content.replace('styleUrls', 'styles').replace('./app.component.css', ''); + }); + await harness.modifyFile('src/app/app.component.html', (content) => { + return `\n${content}`; + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness + .expectFile('dist/browser/main.js') + // div[_ngcontent-%COMP%] {\n -webkit-hyphens: none;\n hyphens: none;\n}\n + .content.toMatch(/{\\n\s*-webkit-hyphens:\s*none;\\n\s*hyphens:\s*none;\\n\s*}/); + }); + + it('should not add prefixes if not required by browsers in inline template styles', async () => { + await harness.writeFile( + '.browserslistrc', + ` + Edge 110 + `, + ); + + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content.replace('styleUrls', 'styles').replace('./app.component.css', ''); + }); + await harness.modifyFile('src/app/app.component.html', (content) => { + return `\n${content}`; + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').content.toMatch(/{\\n\s*hyphens:\s*none;\\n\s*}/); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/typescript-incremental_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/typescript-incremental_spec.ts new file mode 100644 index 000000000000..2c73e66d9f8b --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-incremental_spec.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.io/license + */ + +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "TypeScript explicit incremental option usage"', () => { + it('should successfully build with incremental disabled', async () => { + // Disable tsconfig incremental option in tsconfig + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.incremental = false; + + return JSON.stringify(tsconfig); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/typescript-isolated-modules_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/typescript-isolated-modules_spec.ts new file mode 100644 index 000000000000..06e66cbd6da9 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-isolated-modules_spec.ts @@ -0,0 +1,79 @@ +/** + * @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.io/license + */ + +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "TypeScript isolated modules direct transpilation"', () => { + it('should successfully build with isolated modules enabled and disabled optimizations', async () => { + // Enable tsconfig isolatedModules option in tsconfig + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.isolatedModules = true; + + return JSON.stringify(tsconfig); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + }); + + it('should successfully build with isolated modules enabled and enabled optimizations', async () => { + // Enable tsconfig isolatedModules option in tsconfig + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.isolatedModules = true; + + return JSON.stringify(tsconfig); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + }); + + it('supports TSX files with isolated modules enabled and enabled optimizations', async () => { + // Enable tsconfig isolatedModules option in tsconfig + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.isolatedModules = true; + tsconfig.compilerOptions.jsx = 'react-jsx'; + + return JSON.stringify(tsconfig); + }); + + await harness.writeFile('src/types.d.ts', `declare module 'react/jsx-runtime' { jsx: any }`); + await harness.writeFile('src/abc.tsx', `export function hello() { return

Hello

; }`); + await harness.modifyFile( + 'src/main.ts', + (content) => content + `import { hello } from './abc'; console.log(hello());`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + externalDependencies: ['react'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/typescript-path-mapping_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/typescript-path-mapping_spec.ts new file mode 100644 index 000000000000..41539df239f2 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-path-mapping_spec.ts @@ -0,0 +1,109 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "TypeScript Path Mapping"', () => { + it('should resolve TS files when imported with a path mapping', async () => { + // Change main module import to use path mapping + await harness.modifyFile('src/main.ts', (content) => + content.replace(`'./app/app.module'`, `'@root/app.module'`), + ); + + // Add a path mapping for `@root` + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.paths = { + '@root/*': ['./src/app/*'], + }; + + return JSON.stringify(tsconfig); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + }); + + it('should fail to resolve if no path mapping for an import is present', async () => { + // Change main module import to use path mapping + await harness.modifyFile('src/main.ts', (content) => + content.replace(`'./app/app.module'`, `'@root/app.module'`), + ); + + // Add a path mapping for `@not-root` + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.paths = { + '@not-root/*': ['./src/app/*'], + }; + + return JSON.stringify(tsconfig); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result?.success).toBe(false); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Could not resolve "@root/app.module"'), + }), + ); + }); + + it('should resolve JS files when imported with a path mapping', async () => { + // Change main module import to use path mapping + await harness.modifyFile('src/main.ts', (content) => + content.replace(`'./app/app.module'`, `'app-module'`), + ); + + await harness.writeFiles({ + 'a.js': `export * from './src/app/app.module';\n\nconsole.log('A');`, + 'a.d.ts': `export * from './src/app/app.module';`, + }); + + // Add a path mapping for `@root` + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.paths = { + 'app-module': ['a.js'], + }; + + return JSON.stringify(tsconfig); + }); + + // app.module needs to be manually included since it is not referenced via a TS file + // with the test path mapping in place. + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.files.push('app/app.module.ts'); + + return JSON.stringify(tsconfig); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.toContain(`console.log("A")`); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts new file mode 100644 index 000000000000..c8dd39bfae5d --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts @@ -0,0 +1,92 @@ +/** + * @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 { logging } from '@angular-devkit/core'; +import { concatMap, count, firstValueFrom, take, timeout } from 'rxjs'; +import { buildApplication } from '../../index'; +import { OutputHashing } from '../../schema'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + beforeEach(async () => { + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files = ['main.server.ts', 'main.ts']; + + return JSON.stringify(tsConfig); + }); + + await harness.writeFiles({ + 'src/lazy.ts': `export const foo: number = 1;`, + 'src/main.ts': `export async function fn () { + const lazy = await import('./lazy'); + return lazy.foo; + }`, + 'src/main.server.ts': `export { fn as default } from './main';`, + }); + }); + + describe('Behavior: "Rebuild both server and browser bundles when using lazy loading"', () => { + it('detect changes and errors when expected', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + namedChunks: true, + outputHashing: OutputHashing.None, + server: 'src/main.server.ts', + ssr: true, + }); + + const buildCount = await firstValueFrom( + harness.execute({ outputLogsOnFailure: false }).pipe( + timeout(30_000), + concatMap(async ({ result, logs }, index) => { + switch (index) { + case 0: + expect(result?.success).toBeTrue(); + + // Add valid code + await harness.appendToFile('src/lazy.ts', `console.log('foo');`); + + break; + case 1: + expect(result?.success).toBeTrue(); + + // Update type of 'foo' to invalid (number -> string) + await harness.writeFile('src/lazy.ts', `export const foo: string = 1;`); + + break; + case 2: + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching( + `Type 'number' is not assignable to type 'string'.`, + ), + }), + ); + + // Fix TS error + await harness.writeFile('src/lazy.ts', `export const foo: string = "1";`); + + break; + case 3: + expect(result?.success).toBeTrue(); + + break; + } + }), + take(4), + count(), + ), + ); + + expect(buildCount).toBe(4); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-touch-file_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-touch-file_spec.ts new file mode 100644 index 000000000000..65f0540f2d1b --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-touch-file_spec.ts @@ -0,0 +1,52 @@ +/** + * @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 { concatMap, count, take, timeout } from 'rxjs'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Rebuilds when touching file"', () => { + for (const aot of [true, false]) { + it(`Rebuild correctly when file is touched with ${aot ? 'AOT' : 'JIT'}`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + aot, + }); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(30_000), + concatMap(async ({ result }, index) => { + switch (index) { + case 0: + expect(result?.success).toBeTrue(); + // Touch a file without doing any changes. + await harness.modifyFile('src/app/app.component.ts', (content) => content); + break; + case 1: + expect(result?.success).toBeTrue(); + await harness.removeFile('src/app/app.component.ts'); + break; + case 2: + expect(result?.success).toBeFalse(); + break; + } + }), + take(3), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(3); + }); + } + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts new file mode 100644 index 000000000000..e7d060de1262 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts @@ -0,0 +1,93 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "TypeScript JSON module resolution"', () => { + it('should resolve JSON files when imported with resolveJsonModule enabled', async () => { + await harness.writeFiles({ + 'src/x.json': `{"a": 1}`, + 'src/main.ts': `import * as x from './x.json'; console.log(x);`, + }); + + // Enable tsconfig resolveJsonModule option in tsconfig + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.resolveJsonModule = true; + + return JSON.stringify(tsconfig); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + }); + + it('should fail to resolve with TS if resolveJsonModule is not present', async () => { + await harness.writeFiles({ + 'src/x.json': `{"a": 1}`, + 'src/main.ts': `import * as x from './x.json'; console.log(x);`, + }); + + // Enable tsconfig resolveJsonModule option in tsconfig + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.resolveJsonModule = undefined; + + return JSON.stringify(tsconfig); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result?.success).toBe(false); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(`Cannot find module './x.json'`), + }), + ); + }); + + it('should fail to resolve with TS if resolveJsonModule is disabled', async () => { + await harness.writeFiles({ + 'src/x.json': `{"a": 1}`, + 'src/main.ts': `import * as x from './x.json'; console.log(x);`, + }); + + // Enable tsconfig resolveJsonModule option in tsconfig + await harness.modifyFile('tsconfig.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions.resolveJsonModule = false; + + return JSON.stringify(tsconfig); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result?.success).toBe(false); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(`Cannot find module './x.json'`), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/wasm-esm_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/wasm-esm_spec.ts new file mode 100644 index 000000000000..5ae62f020c1c --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/wasm-esm_spec.ts @@ -0,0 +1,275 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * Compiled and base64 encoded WASM file for the following WAT: + * ``` + * (module + * (export "multiply" (func $multiply)) + * (func $multiply (param i32 i32) (result i32) + * local.get 0 + * local.get 1 + * i32.mul + * ) + * ) + * ``` + */ +const exportWasmBase64 = + 'AGFzbQEAAAABBwFgAn9/AX8DAgEABwwBCG11bHRpcGx5AAAKCQEHACAAIAFsCwAXBG5hbWUBCwEACG11bHRpcGx5AgMBAAA='; +const exportWasmBytes = Buffer.from(exportWasmBase64, 'base64'); + +/** + * Compiled and base64 encoded WASM file for the following WAT: + * ``` + * (module + * (import "./values" "getValue" (func $getvalue (result i32))) + * (export "multiply" (func $multiply)) + * (export "subtract1" (func $subtract)) + * (func $multiply (param i32 i32) (result i32) + * local.get 0 + * local.get 1 + * i32.mul + * ) + * (func $subtract (param i32) (result i32) + * call $getvalue + * local.get 0 + * i32.sub + * ) + * ) + * ``` + */ +const importWasmBase64 = + 'AGFzbQEAAAABEANgAAF/YAJ/fwF/YAF/AX8CFQEILi92YWx1ZXMIZ2V0VmFsdWUAAAMDAgECBxgCCG11bHRpcGx5AAEJc3VidHJhY3QxAAIKEQIHACAAIAFsCwcAEAAgAGsLAC8EbmFtZQEfAwAIZ2V0dmFsdWUBCG11bHRpcGx5AghzdWJ0cmFjdAIHAwAAAQACAA=='; +const importWasmBytes = Buffer.from(importWasmBase64, 'base64'); + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Supports WASM/ES module integration"', () => { + it('should inject initialization code and add an export', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/multiply.wasm', exportWasmBytes); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { multiply } from './multiply.wasm'; + + console.log(multiply(4, 5)); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure initialization code and export name is present in output code + harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate'); + harness.expectFile('dist/browser/main.js').content.toContain('multiply'); + }); + + it('should compile successfully with a provided type definition file', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/multiply.wasm', exportWasmBytes); + await harness.writeFile( + 'src/multiply.wasm.d.ts', + 'export declare function multiply(a: number, b: number): number;', + ); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + import { multiply } from './multiply.wasm'; + + console.log(multiply(4, 5)); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure initialization code and export name is present in output code + harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate'); + harness.expectFile('dist/browser/main.js').content.toContain('multiply'); + }); + + it('should add WASM defined imports and include resolved TS file for import', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/subtract.wasm', importWasmBytes); + + // Create TS file that is expect by WASM file + await harness.writeFile( + 'src/values.ts', + ` + export function getValue(): number { return 100; } + `, + ); + // The file is not imported into any actual TS files so it needs to be manually added to the TypeScript program + await harness.modifyFile('src/tsconfig.app.json', (content) => + content.replace('"main.ts",', '"main.ts","values.ts",'), + ); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { subtract1 } from './subtract.wasm'; + + console.log(subtract1(5)); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure initialization code and export name is present in output code + harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate'); + harness.expectFile('dist/browser/main.js').content.toContain('subtract1'); + harness.expectFile('dist/browser/main.js').content.toContain('./values'); + harness.expectFile('dist/browser/main.js').content.toContain('getValue'); + }); + + it('should add WASM defined imports and include resolved JS file for import', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/subtract.wasm', importWasmBytes); + + // Create JS file that is expect by WASM file + await harness.writeFile( + 'src/values.js', + ` + export function getValue() { return 100; } + `, + ); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { subtract1 } from './subtract.wasm'; + + console.log(subtract1(5)); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure initialization code and export name is present in output code + harness.expectFile('dist/browser/main.js').content.toContain('WebAssembly.instantiate'); + harness.expectFile('dist/browser/main.js').content.toContain('subtract1'); + harness.expectFile('dist/browser/main.js').content.toContain('./values'); + harness.expectFile('dist/browser/main.js').content.toContain('getValue'); + }); + + it('should inline WASM files less than 10kb', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/multiply.wasm', exportWasmBytes); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { multiply } from './multiply.wasm'; + + console.log(multiply(4, 5)); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure WASM is present in output code + harness.expectFile('dist/browser/main.js').content.toContain(exportWasmBase64); + }); + + it('should show an error on invalid WASM file', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // Create WASM file + await harness.writeFile('src/multiply.wasm', 'NOT_WASM'); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { multiply } from './multiply.wasm'; + + console.log(multiply(4, 5)); + `, + ); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Unable to analyze WASM file'), + }), + ); + }); + + it('should show an error if using Zone.js', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['zone.js'], + }); + + // Create WASM file + await harness.writeFile('src/multiply.wasm', importWasmBytes); + + // Create main file that uses the WASM file + await harness.writeFile( + 'src/main.ts', + ` + // @ts-ignore + import { multiply } from './multiply.wasm'; + + console.log(multiply(4, 5)); + `, + ); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching( + 'WASM/ES module integration imports are not supported with Zone.js applications', + ), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts new file mode 100644 index 000000000000..135d5ff68165 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts @@ -0,0 +1,61 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * A regular expression used to check if a built worker is correctly referenced in application code. + */ +const REFERENCED_WORKER_REGEXP = + /new Worker\(new URL\("worker-[A-Z0-9]{8}\.js", import\.meta\.url\)/; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Bundles web worker files within application code"', () => { + it('should use the worker entry point when worker lazy chunks are present', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const workerCodeFile = ` + addEventListener('message', () => { + import('./extra').then((m) => console.log(m.default)); + }); + `; + const extraWorkerCodeFile = ` + export default 'WORKER FILE'; + `; + + // Create a worker file + await harness.writeFile('src/app/worker.ts', workerCodeFile); + await harness.writeFile('src/app/extra.ts', extraWorkerCodeFile); + + // Create app component that uses the directive + await harness.writeFile( + 'src/app/app.component.ts', + ` + import { Component } from '@angular/core' + @Component({ + selector: 'app-root', + standalone: false, + template: '

Worker Test

', + }) + export class AppComponent { + worker = new Worker(new URL('./worker', import.meta.url), { type: 'module' }); + } + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // Ensure built worker is referenced in the application code + harness.expectFile('dist/browser/main.js').content.toMatch(REFERENCED_WORKER_REGEXP); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts b/packages/angular/build/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts new file mode 100644 index 000000000000..ad29d985f712 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts @@ -0,0 +1,208 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Option: "allowedCommonJsDependencies"', () => { + describe('given option is not set', () => { + for (const aot of [true, false]) { + it(`should show warning when depending on a Common JS bundle in ${ + aot ? 'AOT' : 'JIT' + } Mode`, async () => { + // Add a Common JS dependency + await harness.appendToFile('src/app/app.component.ts', `import 'buffer';`); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: [], + optimization: true, + aot, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching( + /Module 'buffer' used by 'src\/app\/app\.component\.ts' is not ESM/, + ), + }), + ); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('base64-js'), + }), + 'Should not warn on transitive CommonJS packages which parent is also CommonJS.', + ); + }); + } + }); + + it('should not show warning when depending on a Common JS bundle which is allowed', async () => { + // Add a Common JS dependency + await harness.appendToFile( + 'src/app/app.component.ts', + ` + import 'buffer'; + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: ['buffer', 'base64-js', 'ieee754'], + optimization: true, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + }); + + it('should not show warning when all dependencies are allowed by wildcard', async () => { + // Add a Common JS dependency + await harness.appendToFile( + 'src/app/app.component.ts', + ` + import 'buffer'; + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: ['*'], + optimization: true, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + }); + + it('should not show warning when depending on zone.js', async () => { + // Add a Common JS dependency + await harness.appendToFile( + 'src/app/app.component.ts', + ` + import 'zone.js'; + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: [], + optimization: true, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + }); + + it(`should not show warning when importing non global local data '@angular/common/locale/fr'`, async () => { + await harness.appendToFile( + 'src/app/app.component.ts', + `import '@angular/common/locales/fr';`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: [], + optimization: true, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + }); + + it('should not show warning in JIT for templateUrl and styleUrl when using paths', async () => { + await harness.modifyFile('tsconfig.json', (content) => { + return content.replace( + /"baseUrl": ".\/",/, + ` + "baseUrl": "./", + "paths": { + "@app/*": [ + "src/app/*" + ] + }, + `, + ); + }); + + await harness.modifyFile('src/app/app.module.ts', (content) => + content.replace('./app.component', '@app/app.component'), + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: [], + optimization: true, + aot: false, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + }); + + it('should not show warning for relative imports', async () => { + await harness.appendToFile('src/main.ts', `import './abc';`); + await harness.writeFile('src/abc.ts', 'console.log("abc");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: [], + optimization: true, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/options/app-shell_spec.ts b/packages/angular/build/src/builders/application/tests/options/app-shell_spec.ts new file mode 100644 index 000000000000..9c8384b29efc --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/options/app-shell_spec.ts @@ -0,0 +1,180 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +const appShellRouteFiles: Record = { + 'src/styles.css': `p { color: #000 }`, + 'src/app/app-shell/app-shell.component.ts': ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-app-shell', + standalone: false, + styles: ['div { color: #fff; }'], + template: '

app-shell works!

', + }) + export class AppShellComponent {}`, + 'src/main.server.ts': ` + import { AppServerModule } from './app/app.module.server'; + export default AppServerModule; + `, + 'src/app/app.module.ts': ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + + import { AppRoutingModule } from './app-routing.module'; + import { AppComponent } from './app.component'; + import { RouterModule } from '@angular/router'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + AppRoutingModule, + RouterModule + ], + bootstrap: [AppComponent] + }) + export class AppModule { } + `, + 'src/app/app.module.server.ts': ` + import { NgModule } from '@angular/core'; + import { ServerModule } from '@angular/platform-server'; + + import { AppModule } from './app.module'; + import { AppComponent } from './app.component'; + import { Routes, RouterModule } from '@angular/router'; + import { AppShellComponent } from './app-shell/app-shell.component'; + + const routes: Routes = [ { path: 'shell', component: AppShellComponent }]; + + @NgModule({ + imports: [ + AppModule, + ServerModule, + RouterModule.forRoot(routes), + ], + bootstrap: [AppComponent], + declarations: [AppShellComponent], + }) + export class AppServerModule {} + `, + 'src/main.ts': ` + import { platformBrowser } from '@angular/platform-browser'; + import { AppModule } from './app/app.module'; + + platformBrowser().bootstrapModule(AppModule).catch(err => console.log(err)); + `, + 'src/app/app-routing.module.ts': ` + import { NgModule } from '@angular/core'; + import { Routes, RouterModule } from '@angular/router'; + + const routes: Routes = []; + + @NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] + }) + export class AppRoutingModule { } + `, + 'src/app/app.component.html': ``, +}; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + beforeEach(async () => { + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('main.server.ts'); + + return JSON.stringify(tsConfig); + }); + + await harness.writeFiles(appShellRouteFiles); + }); + + describe('Option: "appShell"', () => { + it('renders the application shell', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + server: 'src/main.server.ts', + polyfills: ['zone.js'], + appShell: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').toExist(); + const indexFileContent = harness.expectFile('dist/browser/index.html').content; + indexFileContent.toContain('app-shell works!'); + }); + + it('critical CSS is inlined', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + server: 'src/main.server.ts', + polyfills: ['zone.js'], + appShell: true, + styles: ['src/styles.css'], + optimization: { + styles: { + minify: true, + inlineCritical: true, + }, + }, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + const indexFileContent = harness.expectFile('dist/browser/index.html').content; + indexFileContent.toContain('app-shell works!'); + indexFileContent.toContain('p{color:#000}'); + indexFileContent.toContain( + ``, + ); + }); + + it('applies CSP nonce to critical CSS', async () => { + await harness.modifyFile('src/index.html', (content) => + content.replace(/`, + ); + indexFileContent.toContain('`); + }); + + it('should inline critical css when using deployUrl', async () => { + const inlineFontsProcessor = new InlineCriticalCssProcessor({ + readAsset, + deployUrl: 'http://cdn.com/', + }); + + const { content } = await inlineFontsProcessor.process(getContent('http://cdn.com/'), { + outputPath: '/dist/', + }); + + expect(content).toContain( + ``, + ); + expect(content).toContain( + ``, + ); + expect(tags.stripIndents`${content}`).toContain(tags.stripIndents` + `); + }); + + it('should compress inline critical css when minify is enabled', async () => { + const inlineFontsProcessor = new InlineCriticalCssProcessor({ + readAsset, + minify: true, + }); + + const { content } = await inlineFontsProcessor.process(getContent(''), { + outputPath: '/dist/', + }); + + expect(content).toContain( + ``, + ); + expect(content).toContain( + ``, + ); + expect(content).toContain(''); + }); + + it(`should process the inline 'onload' handlers if a 'autoCsp' is true`, async () => { + const inlineCssProcessor = new InlineCriticalCssProcessor({ + readAsset, + autoCsp: true, + }); + + const { content } = await inlineCssProcessor.process(getContent(''), { + outputPath: '/dist/', + }); + + expect(content).toContain( + '', + ); + expect(tags.stripIndents`${content}`).toContain(tags.stripIndents` + `); + }); + + it('should process the inline `onload` handlers if a CSP nonce is specified', async () => { + const inlineCssProcessor = new InlineCriticalCssProcessor({ + readAsset, + }); + + const { content } = await inlineCssProcessor.process( + getContent('', ''), + { + outputPath: '/dist/', + }, + ); + + expect(content).toContain( + '', + ); + expect(content).toContain( + '', + ); + // Nonces shouldn't be added inside the `noscript` tags. + expect(content).toContain(''); + expect(content).toContain(' + + + + + `); + + expect(result).toContain(``); + expect(result).toContain(''); + expect(result).toContain(``); + }); +}); diff --git a/packages/angular/build/src/utils/index-file/valid-self-closing-tags.ts b/packages/angular/build/src/utils/index-file/valid-self-closing-tags.ts new file mode 100644 index 000000000000..f86d556b36f0 --- /dev/null +++ b/packages/angular/build/src/utils/index-file/valid-self-closing-tags.ts @@ -0,0 +1,89 @@ +/** + * @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 list of valid self closing HTML elements */ +export const VALID_SELF_CLOSING_TAGS = new Set([ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', + + /** SVG tags */ + 'animate', + 'animateMotion', + 'animateTransform', + 'circle', + 'ellipse', + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + 'line', + 'path', + 'polygon', + 'polyline', + 'rect', + 'text', + 'tspan', + 'linearGradient', + 'radialGradient', + 'stop', + 'image', + 'pattern', + 'defs', + 'g', + 'marker', + 'mask', + 'style', + 'symbol', + 'use', + 'view', + + /** MathML tags */ + 'mspace', + 'mphantom', + 'mrow', + 'mfrac', + 'msqrt', + 'mroot', + 'mstyle', + 'merror', + 'mpadded', + 'mtable', +]); diff --git a/packages/angular/build/src/utils/index.ts b/packages/angular/build/src/utils/index.ts new file mode 100644 index 000000000000..1a7cb15cd9c3 --- /dev/null +++ b/packages/angular/build/src/utils/index.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 + */ + +export * from './normalize-asset-patterns'; +export * from './normalize-optimization'; +export * from './normalize-source-maps'; +export * from './load-proxy-config'; diff --git a/packages/angular/build/src/utils/load-esm.ts b/packages/angular/build/src/utils/load-esm.ts new file mode 100644 index 000000000000..6a6220f66288 --- /dev/null +++ b/packages/angular/build/src/utils/load-esm.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 + */ + +/** + * Lazily compiled dynamic import loader function. + */ +let load: ((modulePath: string | URL) => Promise) | undefined; + +/** + * 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 function loadEsmModule(modulePath: string | URL): Promise { + load ??= new Function('modulePath', `return import(modulePath);`) as Exclude< + typeof load, + undefined + >; + + return load(modulePath); +} diff --git a/packages/angular/build/src/utils/load-proxy-config.ts b/packages/angular/build/src/utils/load-proxy-config.ts new file mode 100644 index 000000000000..b0882187d0c2 --- /dev/null +++ b/packages/angular/build/src/utils/load-proxy-config.ts @@ -0,0 +1,198 @@ +/** + * @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 { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { extname, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { makeRe as makeRegExpFromGlob } from 'picomatch'; +import { isDynamicPattern } from 'tinyglobby'; +import { assertIsError } from './error'; +import { loadEsmModule } from './load-esm'; + +export async function loadProxyConfiguration( + root: string, + proxyConfig: string | undefined, +): Promise | undefined> { + if (!proxyConfig) { + return undefined; + } + + const proxyPath = resolve(root, proxyConfig); + + if (!existsSync(proxyPath)) { + throw new Error(`Proxy configuration file ${proxyPath} does not exist.`); + } + + let proxyConfiguration; + switch (extname(proxyPath)) { + case '.json': { + const content = await readFile(proxyPath, 'utf-8'); + + const { parse, printParseErrorCode } = await import('jsonc-parser'); + const parseErrors: import('jsonc-parser').ParseError[] = []; + 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 loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath))) + .default; + 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); + break; + } catch (e) { + assertIsError(e); + if (e.code === 'ERR_REQUIRE_ESM') { + // 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 loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath))) + .default; + break; + } + + throw e; + } + } + + return normalizeProxyConfiguration(proxyConfiguration); +} + +/** + * Converts glob patterns to regular expressions to support Vite's proxy option. + * Also converts the Webpack supported array form to an object form supported by both. + * + * @param proxy A proxy configuration object. + */ +function normalizeProxyConfiguration( + proxy: Record | object[], +): Record { + let normalizedProxy: Record | undefined; + + if (Array.isArray(proxy)) { + // Construct an object-form proxy configuration from the array + normalizedProxy = {}; + for (const proxyEntry of proxy) { + if (!('context' in proxyEntry)) { + continue; + } + if (!Array.isArray(proxyEntry.context)) { + continue; + } + + // Array-form entries contain a context string array with the path(s) + // to use for the configuration entry. + const context = proxyEntry.context; + delete proxyEntry.context; + for (const contextEntry of context) { + if (typeof contextEntry !== 'string') { + continue; + } + + normalizedProxy[contextEntry] = proxyEntry; + } + } + } else { + normalizedProxy = proxy; + } + + // TODO: Consider upstreaming glob support + for (const key of Object.keys(normalizedProxy)) { + if (key[0] !== '^' && isDynamicPattern(key)) { + const pattern = makeRegExpFromGlob(key).source; + normalizedProxy[pattern] = normalizedProxy[key]; + delete normalizedProxy[key]; + } + } + + // Replace `pathRewrite` field with a `rewrite` function + for (const proxyEntry of Object.values(normalizedProxy)) { + if ( + typeof proxyEntry === 'object' && + 'pathRewrite' in proxyEntry && + proxyEntry.pathRewrite && + typeof proxyEntry.pathRewrite === 'object' + ) { + // Preprocess path rewrite entries + const pathRewriteEntries: [RegExp, string][] = []; + for (const [pattern, value] of Object.entries( + proxyEntry.pathRewrite as Record, + )) { + pathRewriteEntries.push([new RegExp(pattern), value]); + } + + (proxyEntry as Record).rewrite = pathRewriter.bind( + undefined, + pathRewriteEntries, + ); + + delete proxyEntry.pathRewrite; + } + } + + return normalizedProxy; +} + +function pathRewriter(pathRewriteEntries: [RegExp, string][], path: string): string { + for (const [pattern, value] of pathRewriteEntries) { + const updated = path.replace(pattern, value); + if (path !== updated) { + return updated; + } + } + + return path; +} + +/** + * 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: number, content: string) { + 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 }; +} diff --git a/packages/angular/build/src/utils/load-translations.ts b/packages/angular/build/src/utils/load-translations.ts new file mode 100644 index 000000000000..eeac280cbf9b --- /dev/null +++ b/packages/angular/build/src/utils/load-translations.ts @@ -0,0 +1,83 @@ +/** + * @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 { Diagnostics } from '@angular/localize/tools'; +import { createHash } from 'node:crypto'; +import * as fs from 'node:fs'; +import { loadEsmModule } from './load-esm'; + +export type TranslationLoader = (path: string) => { + translations: Record; + format: string; + locale?: string; + diagnostics: Diagnostics; + integrity: string; +}; + +export async function createTranslationLoader(): Promise { + const { parsers, diagnostics } = await importParsers(); + + return (path: string) => { + const content = fs.readFileSync(path, 'utf8'); + const unusedParsers = new Map(); + for (const [format, parser] of Object.entries(parsers)) { + const analysis = parser.analyze(path, content); + if (analysis.canParse) { + // Types don't overlap here so we need to use any. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { locale, translations } = parser.parse(path, content, analysis.hint as any); + const integrity = 'sha256-' + createHash('sha256').update(content).digest('base64'); + + return { format, locale, translations, diagnostics, integrity }; + } else { + unusedParsers.set(parser, analysis); + } + } + + const messages: string[] = []; + for (const [parser, analysis] of unusedParsers.entries()) { + messages.push(analysis.diagnostics.formatDiagnostics(`*** ${parser.constructor.name} ***`)); + } + throw new Error( + `Unsupported translation file format in ${path}. The following parsers were tried:\n` + + messages.join('\n'), + ); + }; +} + +async function importParsers() { + 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 { + Diagnostics, + ArbTranslationParser, + SimpleJsonTranslationParser, + Xliff1TranslationParser, + Xliff2TranslationParser, + XtbTranslationParser, + } = await loadEsmModule('@angular/localize/tools'); + + const diagnostics = new Diagnostics(); + const parsers = { + arb: new ArbTranslationParser(), + json: new SimpleJsonTranslationParser(), + xlf: new Xliff1TranslationParser(), + xlf2: new Xliff2TranslationParser(), + // The name ('xmb') needs to match the AOT compiler option + xmb: new XtbTranslationParser(), + }; + + return { parsers, diagnostics }; + } catch { + throw new Error( + `Unable to load translation file parsers. Please ensure '@angular/localize' is installed.`, + ); + } +} diff --git a/packages/angular/build/src/utils/normalize-asset-patterns.ts b/packages/angular/build/src/utils/normalize-asset-patterns.ts new file mode 100644 index 000000000000..8a8b2c2cbf1f --- /dev/null +++ b/packages/angular/build/src/utils/normalize-asset-patterns.ts @@ -0,0 +1,82 @@ +/** + * @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 assert from 'node:assert'; +import { statSync } from 'node:fs'; +import * as path from 'node:path'; +import { AssetPattern, AssetPatternClass } from '../builders/application/schema'; + +export class MissingAssetSourceRootException extends Error { + constructor(path: string) { + super(`The ${path} asset path must start with the project source root.`); + } +} + +export function normalizeAssetPatterns( + assetPatterns: AssetPattern[], + workspaceRoot: string, + projectRoot: string, + projectSourceRoot: string | undefined, +): (AssetPatternClass & { output: string })[] { + if (assetPatterns.length === 0) { + return []; + } + + // 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 = 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: string, input: string; + let isDirectory = false; + + try { + isDirectory = statSync(resolvedAssetPath).isDirectory(); + } catch { + isDirectory = true; + } + + if (isDirectory) { + // Folders get a recursive star glob. + glob = '**/*'; + // Input directory is their original path. + input = assetPath; + } else { + // Files are their own glob. + glob = path.basename(assetPath); + // Input directory is their original dirname. + input = path.dirname(assetPath); + } + + // Output directory for both is the relative path from source root to input. + const output = path.relative(resolvedSourceRoot, path.resolve(workspaceRoot, input)); + + assetPattern = { glob, input, output }; + } else { + assetPattern.output = path.join('.', assetPattern.output ?? ''); + } + + assert(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 as AssetPatternClass & { output: string }; + }); +} diff --git a/packages/angular/build/src/utils/normalize-cache.ts b/packages/angular/build/src/utils/normalize-cache.ts new file mode 100644 index 000000000000..f272f6a78e45 --- /dev/null +++ b/packages/angular/build/src/utils/normalize-cache.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 { join, resolve } from 'node:path'; + +/** Version placeholder is replaced during the build process with actual package version */ +const VERSION = '0.0.0-PLACEHOLDER'; + +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; +} + +interface CacheMetadata { + enabled?: boolean; + environment?: 'local' | 'ci' | 'all'; + path?: string; +} + +function hasCacheMetadata(value: unknown): value is { cli: { cache: CacheMetadata } } { + return ( + !!value && + typeof value === 'object' && + 'cli' in value && + !!value['cli'] && + typeof value['cli'] === 'object' && + 'cache' in value['cli'] + ); +} + +export function normalizeCacheOptions( + projectMetadata: unknown, + worspaceRoot: string, +): NormalizedCachedOptions { + const cacheMetadata = hasCacheMetadata(projectMetadata) ? projectMetadata.cli.cache : {}; + + const { + // Webcontainers do not currently benefit from persistent disk caching and can lead to increased browser memory usage + enabled = !process.versions.webcontainer, + 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 = resolve(worspaceRoot, path); + + return { + enabled: cacheEnabled, + basePath: cacheBasePath, + path: join(cacheBasePath, VERSION), + }; +} diff --git a/packages/angular/build/src/utils/normalize-optimization.ts b/packages/angular/build/src/utils/normalize-optimization.ts new file mode 100644 index 000000000000..fcd5b556f27f --- /dev/null +++ b/packages/angular/build/src/utils/normalize-optimization.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 { + FontsClass, + OptimizationClass, + OptimizationUnion, + StylesClass, +} from '../builders/application/schema'; + +export type NormalizedOptimizationOptions = Required< + Omit +> & { + fonts: FontsClass; + styles: StylesClass; +}; + +export function normalizeOptimization( + optimization: OptimizationUnion = true, +): NormalizedOptimizationOptions { + if (typeof optimization === 'object') { + const styleOptimization = !!optimization.styles; + + return { + scripts: !!optimization.scripts, + styles: + typeof optimization.styles === 'object' + ? optimization.styles + : { + minify: styleOptimization, + removeSpecialComments: styleOptimization, + inlineCritical: styleOptimization, + }, + fonts: + typeof optimization.fonts === 'object' + ? optimization.fonts + : { + inline: !!optimization.fonts, + }, + }; + } + + return { + scripts: optimization, + styles: { + minify: optimization, + inlineCritical: optimization, + removeSpecialComments: optimization, + }, + fonts: { + inline: optimization, + }, + }; +} diff --git a/packages/angular/build/src/utils/normalize-source-maps.ts b/packages/angular/build/src/utils/normalize-source-maps.ts new file mode 100644 index 000000000000..cf26ca236bae --- /dev/null +++ b/packages/angular/build/src/utils/normalize-source-maps.ts @@ -0,0 +1,25 @@ +/** + * @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 { SourceMapClass, SourceMapUnion } from '../builders/application/schema'; + +export function normalizeSourceMaps(sourceMap: SourceMapUnion): SourceMapClass { + 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 sourcesContent = typeof sourceMap === 'object' ? sourceMap.sourcesContent : sourceMap; + + return { + vendor, + hidden, + scripts, + styles, + sourcesContent, + }; +} diff --git a/packages/angular/build/src/utils/postcss-configuration.ts b/packages/angular/build/src/utils/postcss-configuration.ts new file mode 100644 index 000000000000..1861f9f2b1db --- /dev/null +++ b/packages/angular/build/src/utils/postcss-configuration.ts @@ -0,0 +1,123 @@ +/** + * @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 { readFile, readdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +export interface PostcssConfiguration { + plugins: [name: string, options?: object | string][]; +} + +interface RawPostcssConfiguration { + plugins?: Record | (string | [string, object])[]; +} + +const postcssConfigurationFiles: string[] = ['postcss.config.json', '.postcssrc.json']; +const tailwindConfigFiles: string[] = [ + 'tailwind.config.js', + 'tailwind.config.cjs', + 'tailwind.config.mjs', + 'tailwind.config.ts', +]; + +export interface SearchDirectory { + root: string; + files: Set; +} + +export async function generateSearchDirectories(roots: string[]): Promise { + return await Promise.all( + roots.map((root) => + readdir(root, { withFileTypes: true }).then((entries) => ({ + root, + files: new Set(entries.filter((entry) => entry.isFile()).map((entry) => entry.name)), + })), + ), + ); +} + +function findFile( + searchDirectories: SearchDirectory[], + potentialFiles: string[], +): string | undefined { + for (const { root, files } of searchDirectories) { + for (const potential of potentialFiles) { + if (files.has(potential)) { + return join(root, potential); + } + } + } + + return undefined; +} + +export function findTailwindConfiguration( + searchDirectories: SearchDirectory[], +): string | undefined { + return findFile(searchDirectories, tailwindConfigFiles); +} + +async function readPostcssConfiguration( + configurationFile: string, +): Promise { + const data = await readFile(configurationFile, 'utf-8'); + const config = JSON.parse(data) as RawPostcssConfiguration; + + return config; +} + +export async function loadPostcssConfiguration( + searchDirectories: SearchDirectory[], +): Promise { + const configPath = findFile(searchDirectories, postcssConfigurationFiles); + if (!configPath) { + return undefined; + } + + const raw = await readPostcssConfiguration(configPath); + + // If no plugins are defined, consider it equivalent to no configuration + if (!raw.plugins || typeof raw.plugins !== 'object') { + return undefined; + } + + // Normalize plugin array form + if (Array.isArray(raw.plugins)) { + if (raw.plugins.length < 1) { + return undefined; + } + + const config: PostcssConfiguration = { plugins: [] }; + for (const element of raw.plugins) { + if (typeof element === 'string') { + config.plugins.push([element]); + } else { + config.plugins.push(element); + } + } + + return config; + } + + // Normalize plugin object map form + const entries = Object.entries(raw.plugins); + if (entries.length < 1) { + return undefined; + } + + const config: PostcssConfiguration = { plugins: [] }; + for (const [name, options] of entries) { + if (!options || (typeof options !== 'object' && typeof options !== 'string')) { + continue; + } + + config.plugins.push([name, options]); + } + + return config; +} diff --git a/packages/angular/build/src/utils/project-metadata.ts b/packages/angular/build/src/utils/project-metadata.ts new file mode 100644 index 000000000000..bc997652fef1 --- /dev/null +++ b/packages/angular/build/src/utils/project-metadata.ts @@ -0,0 +1,37 @@ +/** + * @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 { join } from 'node:path'; + +/** + * Normalize a directory path string. + * Currently only removes a trailing slash if present. + * @param path A path string. + * @returns A normalized path string. + */ +export function normalizeDirectoryPath(path: string): string { + const last = path[path.length - 1]; + if (last === '/' || last === '\\') { + return path.slice(0, -1); + } + + return path; +} + +export function getProjectRootPaths( + workspaceRoot: string, + projectMetadata: { root?: string; sourceRoot?: string }, +) { + const projectRoot = normalizeDirectoryPath(join(workspaceRoot, projectMetadata.root ?? '')); + const rawSourceRoot = projectMetadata.sourceRoot; + const projectSourceRoot = normalizeDirectoryPath( + rawSourceRoot === undefined ? join(projectRoot, 'src') : join(workspaceRoot, rawSourceRoot), + ); + + return { projectRoot, projectSourceRoot }; +} diff --git a/packages/angular/build/src/utils/purge-cache.ts b/packages/angular/build/src/utils/purge-cache.ts new file mode 100644 index 000000000000..5851d052d54a --- /dev/null +++ b/packages/angular/build/src/utils/purge-cache.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 { BuilderContext } from '@angular-devkit/architect'; +import { readdir, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { normalizeCacheOptions } from './normalize-cache'; + +/** Delete stale cache directories used by previous versions of build-angular. */ +export async function purgeStaleBuildCache(context: BuilderContext): Promise { + const projectName = context.target?.project; + if (!projectName) { + return; + } + + const metadata = await context.getProjectMetadata(projectName); + const { basePath, path, enabled } = normalizeCacheOptions(metadata, context.workspaceRoot); + + if (!enabled) { + return; + } + + let baseEntries; + try { + baseEntries = await readdir(basePath, { withFileTypes: true }); + } catch { + // No purging possible if base path does not exist or cannot otherwise be accessed + return; + } + + const entriesToDelete = baseEntries + .filter((d) => d.isDirectory()) + .map((d) => join(basePath, d.name)) + .filter((cachePath) => cachePath !== path) + .map((stalePath) => rm(stalePath, { force: true, recursive: true, maxRetries: 3 })); + + await Promise.allSettled(entriesToDelete); +} diff --git a/packages/angular/build/src/utils/resolve-assets.ts b/packages/angular/build/src/utils/resolve-assets.ts new file mode 100644 index 000000000000..e98879e58de7 --- /dev/null +++ b/packages/angular/build/src/utils/resolve-assets.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 path from 'node:path'; +import { glob } from 'tinyglobby'; + +export async function resolveAssets( + entries: { + glob: string; + ignore?: string[]; + input: string; + output: string; + flatten?: boolean; + followSymlinks?: boolean; + }[], + root: string, +): Promise<{ source: string; destination: string }[]> { + const defaultIgnore = ['.gitkeep', '**/.DS_Store', '**/Thumbs.db']; + + const outputFiles: { source: string; destination: string }[] = []; + + for (const entry of entries) { + const cwd = path.resolve(root, entry.input); + const files = await glob(entry.glob, { + cwd, + dot: true, + ignore: entry.ignore ? defaultIgnore.concat(entry.ignore) : defaultIgnore, + followSymbolicLinks: entry.followSymlinks, + }); + + for (const file of files) { + const src = path.join(cwd, file); + const filePath = entry.flatten ? path.basename(file) : file; + + outputFiles.push({ source: src, destination: path.join(entry.output, filePath) }); + } + } + + return outputFiles; +} diff --git a/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts new file mode 100644 index 000000000000..9e1a657366a0 --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts @@ -0,0 +1,168 @@ +/** + * @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 assert from 'node:assert'; +import { randomUUID } from 'node:crypto'; +import { join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transformer'; + +/** + * @note For some unknown reason, setting `globalThis.ngServerMode = true` does not work when using ESM loader hooks. + */ +const NG_SERVER_MODE_INIT_BYTES = new TextEncoder().encode('var ngServerMode=true;'); + +/** + * Node.js ESM loader to redirect imports to in memory files. + * @see: https://nodejs.org/api/esm.html#loaders for more information about loaders. + */ + +const MEMORY_URL_SCHEME = 'memory://'; + +export interface ESMInMemoryFileLoaderWorkerData { + outputFiles: Record; + workspaceRoot: string; +} + +let memoryVirtualRootUrl: string; +let outputFiles: Record; + +const javascriptTransformer = new JavaScriptTransformer( + // Always enable JIT linking to support applications built with and without AOT. + // In a development environment the additional scope information does not + // have a negative effect unlike production where final output size is relevant. + { sourcemap: true, jit: true }, + 1, +); + +export function initialize(data: ESMInMemoryFileLoaderWorkerData) { + // This path does not actually exist but is used to overlay the in memory files with the + // actual filesystem for resolution purposes. + // A custom URL schema (such as `memory://`) cannot be used for the resolve output because + // the in-memory files may use `import.meta.url` in ways that assume a file URL. + // `createRequire` is one example of this usage. + memoryVirtualRootUrl = pathToFileURL( + join(data.workspaceRoot, `.angular/prerender-root/${randomUUID()}/`), + ).href; + outputFiles = data.outputFiles; +} + +export function resolve( + specifier: string, + context: { parentURL: undefined | string }, + nextResolve: Function, +) { + // In-memory files loaded from external code will contain a memory scheme + if (specifier.startsWith(MEMORY_URL_SCHEME)) { + let memoryUrl; + try { + memoryUrl = new URL(specifier); + } catch { + assert.fail('External code attempted to use malformed memory scheme: ' + specifier); + } + + // Resolve with a URL based from the virtual filesystem root + return { + format: 'module', + shortCircuit: true, + url: new URL(memoryUrl.pathname.slice(1), memoryVirtualRootUrl).href, + }; + } + + // Use next/default resolve if the parent is not from the virtual root + if (!context.parentURL?.startsWith(memoryVirtualRootUrl)) { + return nextResolve(specifier, context); + } + + // Check for `./` and `../` relative specifiers + const isRelative = + specifier[0] === '.' && + (specifier[1] === '/' || (specifier[1] === '.' && specifier[2] === '/')); + + // Relative specifiers from memory file should be based from the parent memory location + if (isRelative) { + let specifierUrl; + try { + specifierUrl = new URL(specifier, context.parentURL); + } catch {} + + if ( + specifierUrl?.pathname && + Object.hasOwn(outputFiles, specifierUrl.href.slice(memoryVirtualRootUrl.length)) + ) { + return { + format: 'module', + shortCircuit: true, + url: specifierUrl.href, + }; + } + + assert.fail( + `In-memory ESM relative file should always exist: '${context.parentURL}' --> '${specifier}'`, + ); + } + + // Update the parent URL to allow for module resolution for the workspace. + // This handles bare specifiers (npm packages) and absolute paths. + // Defer to the next hook in the chain, which would be the + // Node.js default resolve if this is the last user-specified loader. + return nextResolve(specifier, { + ...context, + parentURL: new URL('index.js', memoryVirtualRootUrl).href, + }); +} + +export async function load(url: string, context: { format?: string | null }, nextLoad: Function) { + const { format } = context; + + // Load the file from memory if the URL is based in the virtual root + if (url.startsWith(memoryVirtualRootUrl)) { + const source = outputFiles[url.slice(memoryVirtualRootUrl.length)]; + assert(source !== undefined, 'Resolved in-memory ESM file should always exist: ' + url); + + // In-memory files have already been transformer during bundling and can be returned directly + return { + format, + shortCircuit: true, + source, + }; + } + + // Only module files potentially require transformation. Angular libraries that would + // need linking are ESM only. + if (format === 'module' && isFileProtocol(url)) { + const filePath = fileURLToPath(url); + let source = await javascriptTransformer.transformFile(filePath); + + if (filePath.includes('@angular/')) { + // Prepend 'var ngServerMode=true;' to the source. + source = Buffer.concat([NG_SERVER_MODE_INIT_BYTES, source]); + } + + return { + format, + shortCircuit: true, + source, + }; + } + + // Let Node.js handle all other URLs. + return nextLoad(url); +} + +function isFileProtocol(url: string): boolean { + return url.startsWith('file://'); +} + +function handleProcessExit(): void { + void javascriptTransformer.close(); +} + +process.once('exit', handleProcessExit); +process.once('SIGINT', handleProcessExit); +process.once('uncaughtException', handleProcessExit); diff --git a/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/register-hooks.ts b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/register-hooks.ts new file mode 100644 index 000000000000..b23fe297bc19 --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/register-hooks.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 { register } from 'node:module'; +import { pathToFileURL } from 'node:url'; +import { workerData } from 'node:worker_threads'; + +register('./loader-hooks.js', { parentURL: pathToFileURL(__filename), data: workerData }); diff --git a/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/utils.ts b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/utils.ts new file mode 100644 index 000000000000..3af354f6ba0f --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/utils.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 { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +export const IMPORT_EXEC_ARGV = + '--import=' + pathToFileURL(join(__dirname, 'register-hooks.js')).href; diff --git a/packages/angular/build/src/utils/server-rendering/fetch-patch.ts b/packages/angular/build/src/utils/server-rendering/fetch-patch.ts new file mode 100644 index 000000000000..c099d7dd902c --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/fetch-patch.ts @@ -0,0 +1,72 @@ +/** + * @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 { lookup as lookupMimeType } from 'mrmime'; +import { readFile } from 'node:fs/promises'; +import { extname } from 'node:path'; +import { workerData } from 'node:worker_threads'; + +/** + * This is passed as workerData when setting up the worker via the `piscina` package. + */ +const { assetFiles } = workerData as { + assetFiles: Record; +}; + +const assetsCache: Map; content: Buffer }> = + new Map(); + +export function patchFetchToLoadInMemoryAssets(baseURL: URL): void { + const originalFetch = globalThis.fetch; + const patchedFetch: typeof fetch = async (input, init) => { + let url: URL; + if (input instanceof URL) { + url = input; + } else if (typeof input === 'string') { + url = new URL(input, baseURL); + } else if (typeof input === 'object' && 'url' in input) { + url = new URL(input.url, baseURL); + } else { + return originalFetch(input, init); + } + + const { hostname } = url; + const pathname = decodeURIComponent(url.pathname); + + if (hostname !== baseURL.hostname || !assetFiles[pathname]) { + // Only handle relative requests or files that are in assets. + return originalFetch(input, init); + } + + const cachedAsset = assetsCache.get(pathname); + if (cachedAsset) { + const { content, headers } = cachedAsset; + + return new Response(content, { + headers, + }); + } + + const extension = extname(pathname); + const mimeType = lookupMimeType(extension); + const content = await readFile(assetFiles[pathname]); + const headers = mimeType + ? { + 'Content-Type': mimeType, + } + : undefined; + + assetsCache.set(pathname, { headers, content }); + + return new Response(content, { + headers, + }); + }; + + globalThis.fetch = patchedFetch; +} diff --git a/packages/angular/build/src/utils/server-rendering/launch-server.ts b/packages/angular/build/src/utils/server-rendering/launch-server.ts new file mode 100644 index 000000000000..cfb15b0d979b --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/launch-server.ts @@ -0,0 +1,64 @@ +/** + * @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 assert from 'node:assert'; +import { createServer } from 'node:http'; +import { loadEsmModule } from '../load-esm'; +import { loadEsmModuleFromMemory } from './load-esm-from-memory'; +import { isSsrNodeRequestHandler, isSsrRequestHandler } from './utils'; + +export const DEFAULT_URL = new URL('http://ng-localhost/'); + +/** + * Launches a server that handles local requests. + * + * @returns A promise that resolves to the URL of the running server. + */ +export async function launchServer(): Promise { + const { reqHandler } = await loadEsmModuleFromMemory('./server.mjs'); + const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } = + await loadEsmModule('@angular/ssr/node'); + + if (!isSsrNodeRequestHandler(reqHandler) && !isSsrRequestHandler(reqHandler)) { + return DEFAULT_URL; + } + + const server = createServer((req, res) => { + (async () => { + // handle request + if (isSsrNodeRequestHandler(reqHandler)) { + await reqHandler(req, res, (e) => { + throw e ?? new Error(`Unable to handle request: '${req.url}'.`); + }); + } else { + const webRes = await reqHandler(createWebRequestFromNodeRequest(req)); + if (webRes) { + await writeResponseToNodeResponse(webRes, res); + } else { + res.statusCode = 501; + res.end('Not Implemented.'); + } + } + })().catch((e) => { + res.statusCode = 500; + res.end('Internal Server Error.'); + // eslint-disable-next-line no-console + console.error(e); + }); + }); + + server.unref(); + + await new Promise((resolve) => server.listen(0, 'localhost', resolve)); + + const serverAddress = server.address(); + assert(serverAddress, 'Server address should be defined.'); + assert(typeof serverAddress !== 'string', 'Server address should not be a string.'); + + return new URL(`http://localhost:${serverAddress.port}/`); +} diff --git a/packages/angular/build/src/utils/server-rendering/load-esm-from-memory.ts b/packages/angular/build/src/utils/server-rendering/load-esm-from-memory.ts new file mode 100644 index 000000000000..1d19a07e61de --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/load-esm-from-memory.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 { ApplicationRef, Type } from '@angular/core'; +import type { ɵextractRoutesAndCreateRouteTree, ɵgetOrCreateAngularServerApp } from '@angular/ssr'; +import { assertIsError } from '../error'; +import { loadEsmModule } from '../load-esm'; + +/** + * Represents the exports available from the main server bundle. + */ +interface MainServerBundleExports { + default: (() => Promise) | Type; + ɵextractRoutesAndCreateRouteTree: typeof ɵextractRoutesAndCreateRouteTree; + ɵgetOrCreateAngularServerApp: typeof ɵgetOrCreateAngularServerApp; +} + +/** + * Represents the exports available from the server bundle. + */ +interface ServerBundleExports { + reqHandler?: unknown; +} + +export function loadEsmModuleFromMemory( + path: './main.server.mjs', +): Promise; +export function loadEsmModuleFromMemory(path: './server.mjs'): Promise; +export function loadEsmModuleFromMemory( + path: './main.server.mjs' | './server.mjs', +): Promise { + return loadEsmModule(new URL(path, 'memory://')).catch((e) => { + assertIsError(e); + + // While the error is an 'instanceof Error', it is extended with non transferable properties + // and cannot be transferred from a worker when using `--import`. This results in the error object + // displaying as '[Object object]' when read outside of the worker. Therefore, we reconstruct the error message here. + const error: Error & { code?: string } = new Error(e.message); + error.stack = e.stack; + error.name = e.name; + error.code = e.code; + + throw error; + }) as Promise; +} diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts new file mode 100644 index 000000000000..4d1459e221c2 --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -0,0 +1,238 @@ +/** + * @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 { Metafile } from 'esbuild'; +import { extname } from 'node:path'; +import { runInThisContext } from 'node:vm'; +import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; +import { type BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context'; +import { createOutputFile } from '../../tools/esbuild/utils'; +import { shouldOptimizeChunks } from '../environment-options'; + +export const SERVER_APP_MANIFEST_FILENAME = 'angular-app-manifest.mjs'; +export const SERVER_APP_ENGINE_MANIFEST_FILENAME = 'angular-app-engine-manifest.mjs'; + +interface FilesMapping { + path: string; + dynamicImport: boolean; +} + +const MAIN_SERVER_OUTPUT_FILENAME = 'main.server.mjs'; + +/** + * A mapping of unsafe characters to their escaped Unicode equivalents. + */ +const UNSAFE_CHAR_MAP: Record = { + '`': '\\`', + '$': '\\$', + '\\': '\\\\', +}; + +/** + * Escapes unsafe characters in a given string by replacing them with + * their Unicode escape sequences. + * + * @param str - The string to be escaped. + * @returns The escaped string where unsafe characters are replaced. + */ +function escapeUnsafeChars(str: string): string { + return str.replace(/[$`\\]/g, (c) => UNSAFE_CHAR_MAP[c]); +} + +/** + * Generates the server manifest for the App Engine environment. + * + * This manifest is used to configure the server-side rendering (SSR) setup for the + * Angular application when deployed to Google App Engine. It includes the entry points + * for different locales and the base HREF for the application. + * + * @param i18nOptions - The internationalization options for the application build. This + * includes settings for inlining locales and determining the output structure. + * @param baseHref - The base HREF for the application. This is used to set the base URL + * for all relative URLs in the application. + */ +export function generateAngularServerAppEngineManifest( + i18nOptions: NormalizedApplicationBuildOptions['i18nOptions'], + baseHref: string | undefined, +): string { + const entryPoints: Record = {}; + const supportedLocales: Record = {}; + + if (i18nOptions.shouldInline && !i18nOptions.flatOutput) { + for (const locale of i18nOptions.inlineLocales) { + const { subPath } = i18nOptions.locales[locale]; + const importPath = `${subPath ? `${subPath}/` : ''}${MAIN_SERVER_OUTPUT_FILENAME}`; + entryPoints[subPath] = `() => import('./${importPath}')`; + supportedLocales[locale] = subPath; + } + } else { + entryPoints[''] = `() => import('./${MAIN_SERVER_OUTPUT_FILENAME}')`; + supportedLocales[i18nOptions.sourceLocale] = ''; + } + + // Remove trailing slash but retain leading slash. + let basePath = baseHref || '/'; + if (basePath.length > 1 && basePath[basePath.length - 1] === '/') { + basePath = basePath.slice(0, -1); + } + + const manifestContent = ` +export default { + basePath: '${basePath}', + supportedLocales: ${JSON.stringify(supportedLocales, undefined, 2)}, + entryPoints: { + ${Object.entries(entryPoints) + .map(([key, value]) => `'${key}': ${value}`) + .join(',\n ')} + }, +}; +`; + + return manifestContent; +} + +/** + * Generates the server manifest for the standard Node.js environment. + * + * This manifest is used to configure the server-side rendering (SSR) setup for the + * Angular application when running in a standard Node.js environment. It includes + * information about the bootstrap module, whether to inline critical CSS, and any + * additional HTML and CSS output files. + * + * @param additionalHtmlOutputFiles - A map of additional HTML output files generated + * during the build process, keyed by their file paths. + * @param outputFiles - An array of all output files from the build process, including + * JavaScript and CSS files. + * @param inlineCriticalCss - A boolean indicating whether critical CSS should be inlined + * in the server-side rendered pages. + * @param routes - An optional array of route definitions for the application, used for + * server-side rendering and routing. + * @param locale - An optional string representing the locale or language code to be used for + * the application, helping with localization and rendering content specific to the locale. + * @param baseHref - The base HREF for the application. This is used to set the base URL + * for all relative URLs in the application. + * @param initialFiles - A list of initial files that preload tags have already been added for. + * @param metafile - An esbuild metafile object. + * @param publicPath - The configured public path. + * + * @returns An object containing: + * - `manifestContent`: A string of the SSR manifest content. + * - `serverAssetsChunks`: An array of build output files containing the generated assets for the server. + */ +export function generateAngularServerAppManifest( + additionalHtmlOutputFiles: Map, + outputFiles: BuildOutputFile[], + inlineCriticalCss: boolean, + routes: readonly unknown[] | undefined, + locale: string | undefined, + baseHref: string, + initialFiles: Set, + metafile: Metafile, + publicPath: string | undefined, +): { + manifestContent: string; + serverAssetsChunks: BuildOutputFile[]; +} { + const serverAssetsChunks: BuildOutputFile[] = []; + const serverAssets: Record = {}; + + for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) { + const extension = extname(file.path); + if (extension === '.html' || (inlineCriticalCss && extension === '.css')) { + const jsChunkFilePath = `assets-chunks/${file.path.replace(/[./]/g, '_')}.mjs`; + const escapedContent = escapeUnsafeChars(file.text); + + serverAssetsChunks.push( + createOutputFile( + jsChunkFilePath, + `export default \`${escapedContent}\`;`, + BuildOutputFileType.ServerApplication, + ), + ); + + // This is needed because JavaScript engines script parser convert `\r\n` to `\n` in template literals, + // which can result in an incorrect byte length. + const size = runInThisContext(`new TextEncoder().encode(\`${escapedContent}\`).byteLength`); + + serverAssets[file.path] = + `{size: ${size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}`; + } + } + + // When routes have been extracted, mappings are no longer needed, as preloads will be included in the metadata. + // When shouldOptimizeChunks is enabled the metadata is no longer correct and thus we cannot generate the mappings. + const entryPointToBrowserMapping = + routes?.length || shouldOptimizeChunks + ? undefined + : generateLazyLoadedFilesMappings(metafile, initialFiles, publicPath); + + const manifestContent = ` +export default { + bootstrap: () => import('./main.server.mjs').then(m => m.default), + inlineCriticalCss: ${inlineCriticalCss}, + baseHref: '${baseHref}', + locale: ${JSON.stringify(locale)}, + routes: ${JSON.stringify(routes, undefined, 2)}, + entryPointToBrowserMapping: ${JSON.stringify(entryPointToBrowserMapping, undefined, 2)}, + assets: { + ${Object.entries(serverAssets) + .map(([key, value]) => `'${key}': ${value}`) + .join(',\n ')} + }, +}; +`; + + return { manifestContent, serverAssetsChunks }; +} + +/** + * Maps entry points to their corresponding browser bundles for lazy loading. + * + * This function processes a metafile's outputs to generate a mapping between browser-side entry points + * and the associated JavaScript files that should be loaded in the browser. It includes the entry-point's + * own path and any valid imports while excluding initial files or external resources. + */ +function generateLazyLoadedFilesMappings( + metafile: Metafile, + initialFiles: Set, + publicPath = '', +): Record { + const entryPointToBundles: Record = {}; + for (const [fileName, { entryPoint, exports, imports }] of Object.entries(metafile.outputs)) { + // Skip files that don't have an entryPoint, no exports, or are not .js + if (!entryPoint || exports?.length < 1 || !fileName.endsWith('.js')) { + continue; + } + + const importedPaths: FilesMapping[] = [ + { + path: `${publicPath}${fileName}`, + dynamicImport: false, + }, + ]; + + for (const { kind, external, path } of imports) { + if ( + external || + initialFiles.has(path) || + (kind !== 'dynamic-import' && kind !== 'import-statement') + ) { + continue; + } + + importedPaths.push({ + path: `${publicPath}${path}`, + dynamicImport: kind === 'dynamic-import', + }); + } + + entryPointToBundles[entryPoint] = importedPaths; + } + + return entryPointToBundles; +} diff --git a/packages/angular/build/src/utils/server-rendering/models.ts b/packages/angular/build/src/utils/server-rendering/models.ts new file mode 100644 index 000000000000..9a9020d2db7f --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/models.ts @@ -0,0 +1,40 @@ +/** + * @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 { RenderMode, ɵextractRoutesAndCreateRouteTree } from '@angular/ssr'; +import { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks'; + +type Writeable = T extends readonly (infer U)[] ? U[] : never; + +export interface RoutesExtractorWorkerData extends ESMInMemoryFileLoaderWorkerData { + assetFiles: Record; +} + +export type SerializableRouteTreeNode = ReturnType< + Awaited>['routeTree']['toObject'] +>; + +export type WritableSerializableRouteTreeNode = Writeable; + +export interface RoutersExtractorWorkerResult { + serializedRouteTree: SerializableRouteTreeNode; + appShellRoute?: string; + errors: string[]; +} + +/** + * Local copy of `RenderMode` exported from `@angular/ssr`. + * This constant is needed to handle interop between CommonJS (CJS) and ES Modules (ESM) formats. + * + * It maps `RenderMode` enum values to their corresponding numeric identifiers. + */ +export const RouteRenderMode: Record = { + Server: 0, + Client: 1, + Prerender: 2, +}; diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts new file mode 100644 index 000000000000..a8a42c7c941a --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -0,0 +1,406 @@ +/** + * @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 { readFile } from 'node:fs/promises'; +import { extname, posix } from 'node:path'; +import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; +import { OutputMode } from '../../builders/application/schema'; +import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context'; +import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result'; +import { assertIsError } from '../error'; +import { urlJoin } from '../url'; +import { WorkerPool } from '../worker-pool'; +import { IMPORT_EXEC_ARGV } from './esm-in-memory-loader/utils'; +import { SERVER_APP_MANIFEST_FILENAME } from './manifest'; +import { + RouteRenderMode, + RoutersExtractorWorkerResult, + RoutesExtractorWorkerData, + SerializableRouteTreeNode, + WritableSerializableRouteTreeNode, +} from './models'; +import type { RenderWorkerData } from './render-worker'; + +type PrerenderOptions = NormalizedApplicationBuildOptions['prerenderOptions']; +type AppShellOptions = NormalizedApplicationBuildOptions['appShellOptions']; + +/** + * Represents the output of a prerendering process. + * + * The key is the file path, and the value is an object containing the following properties: + * + * - `content`: The HTML content or output generated for the corresponding file path. + * - `appShellRoute`: A boolean flag indicating whether the content is an app shell. + * + * @example + * { + * '/index.html': { content: '...', appShell: false }, + * '/shell/index.html': { content: '...', appShellRoute: true } + * } + */ +type PrerenderOutput = Record; + +export async function prerenderPages( + workspaceRoot: string, + baseHref: string, + appShellOptions: AppShellOptions | undefined, + prerenderOptions: PrerenderOptions | undefined, + outputFiles: Readonly, + assets: Readonly, + outputMode: OutputMode | undefined, + sourcemap = false, + maxThreads = 1, +): Promise<{ + output: PrerenderOutput; + warnings: string[]; + errors: string[]; + serializableRouteTreeNode: SerializableRouteTreeNode; +}> { + const outputFilesForWorker: Record = {}; + const serverBundlesSourceMaps = new Map(); + const warnings: string[] = []; + const errors: string[] = []; + + for (const { text, path, type } of outputFiles) { + if (type !== BuildOutputFileType.ServerApplication && type !== BuildOutputFileType.ServerRoot) { + continue; + } + + // Contains the server runnable application code + if (extname(path) === '.map') { + serverBundlesSourceMaps.set(path.slice(0, -4), text); + } else { + outputFilesForWorker[path] = text; + } + } + + // Inline sourcemap into JS file. This is needed to make Node.js resolve sourcemaps + // when using `--enable-source-maps` when using in memory files. + for (const [filePath, map] of serverBundlesSourceMaps) { + const jsContent = outputFilesForWorker[filePath]; + if (jsContent) { + outputFilesForWorker[filePath] = + jsContent + + `\n//# sourceMappingURL=` + + `data:application/json;base64,${Buffer.from(map).toString('base64')}`; + } + } + serverBundlesSourceMaps.clear(); + + const assetsReversed: Record = {}; + for (const { source, destination } of assets) { + assetsReversed[addLeadingSlash(destination.replace(/\\/g, posix.sep))] = source; + } + + // Get routes to prerender + const { + errors: extractionErrors, + serializedRouteTree: serializableRouteTreeNode, + appShellRoute, + } = await getAllRoutes( + workspaceRoot, + baseHref, + outputFilesForWorker, + assetsReversed, + appShellOptions, + prerenderOptions, + sourcemap, + outputMode, + ).catch((err) => { + return { + errors: [`An error occurred while extracting routes.\n\n${err.message ?? err.stack ?? err}`], + serializedRouteTree: [], + appShellRoute: undefined, + }; + }); + + errors.push(...extractionErrors); + + const serializableRouteTreeNodeForPrerender: WritableSerializableRouteTreeNode = []; + for (const metadata of serializableRouteTreeNode) { + if (outputMode !== OutputMode.Static && metadata.redirectTo) { + // Skip redirects if output mode is not static. + continue; + } + + if (metadata.route.includes('*')) { + // Skip catch all routes from prerender. + continue; + } + + switch (metadata.renderMode) { + case undefined: /* Legacy building mode */ + case RouteRenderMode.Prerender: + serializableRouteTreeNodeForPrerender.push(metadata); + break; + case RouteRenderMode.Server: + if (outputMode === OutputMode.Static) { + errors.push( + `Route '${metadata.route}' is configured with server render mode, but the build 'outputMode' is set to 'static'.`, + ); + } + break; + } + } + + if (!serializableRouteTreeNodeForPrerender.length || errors.length > 0) { + return { + errors, + warnings, + output: {}, + serializableRouteTreeNode, + }; + } + + // Add the extracted routes to the manifest file. + // We could re-generate it from the start, but that would require a number of options to be passed down. + const manifest = outputFilesForWorker[SERVER_APP_MANIFEST_FILENAME]; + if (manifest) { + outputFilesForWorker[SERVER_APP_MANIFEST_FILENAME] = manifest.replace( + 'routes: undefined,', + `routes: ${JSON.stringify(serializableRouteTreeNodeForPrerender, undefined, 2)},`, + ); + } + + // Render routes + const { errors: renderingErrors, output } = await renderPages( + baseHref, + sourcemap, + serializableRouteTreeNodeForPrerender, + maxThreads, + workspaceRoot, + outputFilesForWorker, + assetsReversed, + outputMode, + appShellRoute ?? appShellOptions?.route, + ); + + errors.push(...renderingErrors); + + return { + errors, + warnings, + output, + serializableRouteTreeNode, + }; +} + +async function renderPages( + baseHref: string, + sourcemap: boolean, + serializableRouteTreeNode: SerializableRouteTreeNode, + maxThreads: number, + workspaceRoot: string, + outputFilesForWorker: Record, + assetFilesForWorker: Record, + outputMode: OutputMode | undefined, + appShellRoute: string | undefined, +): Promise<{ + output: PrerenderOutput; + errors: string[]; +}> { + const output: PrerenderOutput = {}; + const errors: string[] = []; + const workerExecArgv = [IMPORT_EXEC_ARGV]; + + if (sourcemap) { + workerExecArgv.push('--enable-source-maps'); + } + + const renderWorker = new WorkerPool({ + filename: require.resolve('./render-worker'), + maxThreads: Math.min(serializableRouteTreeNode.length, maxThreads), + workerData: { + workspaceRoot, + outputFiles: outputFilesForWorker, + assetFiles: assetFilesForWorker, + outputMode, + hasSsrEntry: !!outputFilesForWorker['server.mjs'], + } as RenderWorkerData, + execArgv: workerExecArgv, + }); + + try { + const renderingPromises: Promise[] = []; + const appShellRouteWithLeadingSlash = appShellRoute && addLeadingSlash(appShellRoute); + const baseHrefPathnameWithLeadingSlash = new URL(baseHref, 'http://localhost/').pathname; + + for (const { route, redirectTo } of serializableRouteTreeNode) { + // Remove the base href from the file output path. + const routeWithoutBaseHref = addTrailingSlash(route).startsWith( + baseHrefPathnameWithLeadingSlash, + ) + ? addLeadingSlash(route.slice(baseHrefPathnameWithLeadingSlash.length)) + : route; + + const outPath = posix.join(removeLeadingSlash(routeWithoutBaseHref), 'index.html'); + + if (typeof redirectTo === 'string') { + output[outPath] = { content: generateRedirectStaticPage(redirectTo), appShellRoute: false }; + + continue; + } + + const render: Promise = renderWorker.run({ url: route }); + const renderResult: Promise = render + .then((content) => { + if (content !== null) { + output[outPath] = { + content, + appShellRoute: appShellRouteWithLeadingSlash === routeWithoutBaseHref, + }; + } + }) + .catch((err) => { + errors.push( + `An error occurred while prerendering route '${route}'.\n\n${err.message ?? err.stack ?? err.code ?? err}`, + ); + void renderWorker.destroy(); + }); + + renderingPromises.push(renderResult); + } + + await Promise.all(renderingPromises); + } finally { + void renderWorker.destroy(); + } + + return { + errors, + output, + }; +} + +async function getAllRoutes( + workspaceRoot: string, + baseHref: string, + outputFilesForWorker: Record, + assetFilesForWorker: Record, + appShellOptions: AppShellOptions | undefined, + prerenderOptions: PrerenderOptions | undefined, + sourcemap: boolean, + outputMode: OutputMode | undefined, +): Promise<{ + serializedRouteTree: SerializableRouteTreeNode; + appShellRoute?: string; + errors: string[]; +}> { + const { routesFile, discoverRoutes } = prerenderOptions ?? {}; + const routes: WritableSerializableRouteTreeNode = []; + let appShellRoute: string | undefined; + + if (appShellOptions) { + appShellRoute = urlJoin(baseHref, appShellOptions.route); + + routes.push({ + renderMode: RouteRenderMode.Prerender, + route: appShellRoute, + }); + } + + if (routesFile) { + const routesFromFile = (await readFile(routesFile, 'utf8')).split(/\r?\n/); + for (const route of routesFromFile) { + routes.push({ + renderMode: RouteRenderMode.Prerender, + route: urlJoin(baseHref, route.trim()), + }); + } + } + + if (!discoverRoutes) { + return { errors: [], appShellRoute, serializedRouteTree: routes }; + } + + const workerExecArgv = [IMPORT_EXEC_ARGV]; + + if (sourcemap) { + workerExecArgv.push('--enable-source-maps'); + } + + const renderWorker = new WorkerPool({ + filename: require.resolve('./routes-extractor-worker'), + maxThreads: 1, + workerData: { + workspaceRoot, + outputFiles: outputFilesForWorker, + assetFiles: assetFilesForWorker, + outputMode, + hasSsrEntry: !!outputFilesForWorker['server.mjs'], + } as RoutesExtractorWorkerData, + execArgv: workerExecArgv, + }); + + try { + const { serializedRouteTree, appShellRoute, errors }: RoutersExtractorWorkerResult = + await renderWorker.run({}); + + if (!routes.length) { + return { errors, appShellRoute, serializedRouteTree }; + } + + // Merge the routing trees + const uniqueRoutes = new Map(); + for (const item of [...routes, ...serializedRouteTree]) { + if (!uniqueRoutes.has(item.route)) { + uniqueRoutes.set(item.route, item); + } + } + + return { errors, serializedRouteTree: Array.from(uniqueRoutes.values()) }; + } catch (err) { + assertIsError(err); + + return { + errors: [ + `An error occurred while extracting routes.\n\n${err.message ?? err.stack ?? err.code ?? err}`, + ], + serializedRouteTree: [], + }; + } finally { + void renderWorker.destroy(); + } +} + +function addLeadingSlash(value: string): string { + return value[0] === '/' ? value : '/' + value; +} + +function addTrailingSlash(url: string): string { + return url[url.length - 1] === '/' ? url : `${url}/`; +} + +function removeLeadingSlash(value: string): string { + return value[0] === '/' ? value.slice(1) : value; +} + +/** + * Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL. + * + * This function creates a simple HTML page that performs a redirect using a meta tag. + * It includes a fallback link in case the meta-refresh doesn't work. + * + * @param url - The URL to which the page should redirect. + * @returns The HTML content of the static redirect page. + */ +function generateRedirectStaticPage(url: string): string { + return ` + + + + + Redirecting + + + +
Redirecting to ${url}
+ + +`.trim(); +} diff --git a/packages/angular/build/src/utils/server-rendering/render-worker.ts b/packages/angular/build/src/utils/server-rendering/render-worker.ts new file mode 100644 index 000000000000..92fad35df32a --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/render-worker.ts @@ -0,0 +1,64 @@ +/** + * @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 { workerData } from 'node:worker_threads'; +import type { OutputMode } from '../../builders/application/schema'; +import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks'; +import { patchFetchToLoadInMemoryAssets } from './fetch-patch'; +import { DEFAULT_URL, launchServer } from './launch-server'; +import { loadEsmModuleFromMemory } from './load-esm-from-memory'; + +export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData { + assetFiles: Record; + outputMode: OutputMode | undefined; + hasSsrEntry: boolean; +} + +export interface RenderOptions { + url: string; +} + +/** + * This is passed as workerData when setting up the worker via the `piscina` package. + */ +const { outputMode, hasSsrEntry } = workerData as { + outputMode: OutputMode | undefined; + hasSsrEntry: boolean; +}; + +let serverURL = DEFAULT_URL; + +/** + * Renders each route in routes and writes them to //index.html. + */ +async function renderPage({ url }: RenderOptions): Promise { + const { ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp } = + await loadEsmModuleFromMemory('./main.server.mjs'); + + const angularServerApp = getOrCreateAngularServerApp({ + allowStaticRouteRender: true, + }); + + const response = await angularServerApp.handle( + new Request(new URL(url, serverURL), { signal: AbortSignal.timeout(30_000) }), + ); + + return response ? response.text() : null; +} + +async function initialize() { + if (outputMode !== undefined && hasSsrEntry) { + serverURL = await launchServer(); + } + + patchFetchToLoadInMemoryAssets(serverURL); + + return renderPage; +} + +export default initialize(); diff --git a/packages/angular/build/src/utils/server-rendering/routes-extractor-worker.ts b/packages/angular/build/src/utils/server-rendering/routes-extractor-worker.ts new file mode 100644 index 000000000000..1487db07e811 --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/routes-extractor-worker.ts @@ -0,0 +1,52 @@ +/** + * @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 { workerData } from 'node:worker_threads'; +import { OutputMode } from '../../builders/application/schema'; +import { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks'; +import { patchFetchToLoadInMemoryAssets } from './fetch-patch'; +import { DEFAULT_URL, launchServer } from './launch-server'; +import { loadEsmModuleFromMemory } from './load-esm-from-memory'; +import { RoutersExtractorWorkerResult } from './models'; + +export interface ExtractRoutesWorkerData extends ESMInMemoryFileLoaderWorkerData { + outputMode: OutputMode | undefined; +} + +/** + * This is passed as workerData when setting up the worker via the `piscina` package. + */ +const { outputMode, hasSsrEntry } = workerData as { + outputMode: OutputMode | undefined; + hasSsrEntry: boolean; +}; + +/** Renders an application based on a provided options. */ +async function extractRoutes(): Promise { + const serverURL = outputMode !== undefined && hasSsrEntry ? await launchServer() : DEFAULT_URL; + + patchFetchToLoadInMemoryAssets(serverURL); + + const { ɵextractRoutesAndCreateRouteTree: extractRoutesAndCreateRouteTree } = + await loadEsmModuleFromMemory('./main.server.mjs'); + + const { routeTree, appShellRoute, errors } = await extractRoutesAndCreateRouteTree({ + url: serverURL, + invokeGetPrerenderParams: outputMode !== undefined, + includePrerenderFallbackRoutes: outputMode === OutputMode.Server, + signal: AbortSignal.timeout(30_000), + }); + + return { + errors, + appShellRoute, + serializedRouteTree: routeTree.toObject(), + }; +} + +export default extractRoutes; diff --git a/packages/angular/build/src/utils/server-rendering/utils.ts b/packages/angular/build/src/utils/server-rendering/utils.ts new file mode 100644 index 000000000000..83c90187178b --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/utils.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 { createRequestHandler } from '@angular/ssr'; +import type { createNodeRequestHandler } from '@angular/ssr/node'; + +export function isSsrNodeRequestHandler( + value: unknown, +): value is ReturnType { + return typeof value === 'function' && '__ng_node_request_handler__' in value; +} +export function isSsrRequestHandler( + value: unknown, +): value is ReturnType { + return typeof value === 'function' && '__ng_request_handler__' in value; +} diff --git a/packages/angular/build/src/utils/service-worker.ts b/packages/angular/build/src/utils/service-worker.ts new file mode 100644 index 000000000000..f9d5c14d27fd --- /dev/null +++ b/packages/angular/build/src/utils/service-worker.ts @@ -0,0 +1,251 @@ +/** + * @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 { Config, Filesystem } from '@angular/service-worker/config'; +import * as crypto from 'node:crypto'; +import { existsSync, constants as fsConstants, promises as fsPromises } from 'node:fs'; +import * as path from 'node:path'; +import { BuildOutputFile, BuildOutputFileType } from '../tools/esbuild/bundler-context'; +import { BuildOutputAsset } from '../tools/esbuild/bundler-execution-result'; +import { assertIsError } from './error'; +import { loadEsmModule } from './load-esm'; + +class CliFilesystem implements Filesystem { + constructor( + private fs: typeof fsPromises, + private base: string, + ) {} + + list(dir: string): Promise { + return this._recursiveList(this._resolve(dir), []); + } + + read(file: string): Promise { + return this.fs.readFile(this._resolve(file), 'utf-8'); + } + + async hash(file: string): Promise { + return crypto + .createHash('sha1') + .update(await this.fs.readFile(this._resolve(file))) + .digest('hex'); + } + + write(_file: string, _content: string): never { + throw new Error('This should never happen.'); + } + + private _resolve(file: string): string { + return path.join(this.base, file); + } + + private async _recursiveList(dir: string, items: string[]): Promise { + const subdirectories = []; + for (const entry of await this.fs.readdir(dir)) { + const entryPath = path.join(dir, entry); + const stats = await this.fs.stat(entryPath); + + if (stats.isFile()) { + // Uses posix paths since the service worker expects URLs + items.push('/' + path.relative(this.base, entryPath).replace(/\\/g, '/')); + } else if (stats.isDirectory()) { + subdirectories.push(entryPath); + } + } + + for (const subdirectory of subdirectories) { + await this._recursiveList(subdirectory, items); + } + + return items; + } +} + +class ResultFilesystem implements Filesystem { + private readonly fileReaders = new Map Promise>(); + + constructor( + outputFiles: BuildOutputFile[], + assetFiles: { source: string; destination: string }[], + ) { + for (const file of outputFiles) { + if (file.type === BuildOutputFileType.Media || file.type === BuildOutputFileType.Browser) { + this.fileReaders.set('/' + file.path.replace(/\\/g, '/'), async () => file.contents); + } + } + for (const file of assetFiles) { + this.fileReaders.set('/' + file.destination.replace(/\\/g, '/'), () => + fsPromises.readFile(file.source), + ); + } + } + + async list(dir: string): Promise { + if (dir !== '/') { + throw new Error('Serviceworker manifest generator should only list files from root.'); + } + + return [...this.fileReaders.keys()]; + } + + async read(file: string): Promise { + const reader = this.fileReaders.get(file); + if (reader === undefined) { + throw new Error('File does not exist.'); + } + const contents = await reader(); + + return Buffer.from(contents.buffer, contents.byteOffset, contents.byteLength).toString('utf-8'); + } + + async hash(file: string): Promise { + const reader = this.fileReaders.get(file); + if (reader === undefined) { + throw new Error('File does not exist.'); + } + + return crypto + .createHash('sha1') + .update(await reader()) + .digest('hex'); + } + + write(): never { + throw new Error('Serviceworker manifest generator should not attempted to write.'); + } +} + +export async function augmentAppWithServiceWorker( + appRoot: string, + workspaceRoot: string, + outputPath: string, + baseHref: string, + ngswConfigPath?: string, + inputFileSystem = fsPromises, + outputFileSystem = fsPromises, +): Promise { + // Determine the configuration file path + const configPath = ngswConfigPath + ? path.join(workspaceRoot, ngswConfigPath) + : path.join(appRoot, 'ngsw-config.json'); + + // Read the configuration file + let config: Config | undefined; + try { + const configurationData = await inputFileSystem.readFile(configPath, 'utf-8'); + config = JSON.parse(configurationData) as Config; + } catch (error) { + assertIsError(error); + if (error.code === 'ENOENT') { + throw new Error( + 'Error: Expected to find an ngsw-config.json configuration file' + + ` in the ${appRoot} folder. Either provide one or` + + ' disable Service Worker in the angular.json configuration file.', + ); + } else { + throw error; + } + } + + const result = await augmentAppWithServiceWorkerCore( + config, + new CliFilesystem(outputFileSystem, outputPath), + baseHref, + ); + + const copy = async (src: string, dest: string): Promise => { + const resolvedDest = path.join(outputPath, dest); + + return outputFileSystem.writeFile(resolvedDest, await inputFileSystem.readFile(src)); + }; + + await outputFileSystem.writeFile(path.join(outputPath, 'ngsw.json'), result.manifest); + + for (const { source, destination } of result.assetFiles) { + await copy(source, destination); + } +} + +// This is currently used by the esbuild-based builder +export async function augmentAppWithServiceWorkerEsbuild( + workspaceRoot: string, + configPath: string, + baseHref: string, + indexHtml: string | undefined, + outputFiles: BuildOutputFile[], + assetFiles: BuildOutputAsset[], +): Promise<{ manifest: string; assetFiles: BuildOutputAsset[] }> { + // Read the configuration file + let config: Config | undefined; + try { + const configurationData = await fsPromises.readFile(configPath, 'utf-8'); + config = JSON.parse(configurationData) as Config; + + if (indexHtml) { + config.index = indexHtml; + } + } catch (error) { + assertIsError(error); + if (error.code === 'ENOENT') { + // TODO: Generate an error object that can be consumed by the esbuild-based builder + const message = `Service worker configuration file "${path.relative( + workspaceRoot, + configPath, + )}" could not be found.`; + throw new Error(message); + } else { + throw error; + } + } + + return augmentAppWithServiceWorkerCore( + config, + new ResultFilesystem(outputFiles, assetFiles), + baseHref, + ); +} + +export async function augmentAppWithServiceWorkerCore( + config: Config, + serviceWorkerFilesystem: Filesystem, + baseHref: string, +): Promise<{ manifest: string; assetFiles: { source: string; destination: string }[] }> { + // Load ESM `@angular/service-worker/config` 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 GeneratorConstructor = ( + await loadEsmModule( + '@angular/service-worker/config', + ) + ).Generator; + + // Generate the manifest + const generator = new GeneratorConstructor(serviceWorkerFilesystem, baseHref); + const output = await generator.process(config); + + // Write the manifest + const manifest = JSON.stringify(output, null, 2); + + // Find the service worker package + const workerPath = require.resolve('@angular/service-worker/ngsw-worker.js'); + + const result = { + manifest, + // Main worker code + assetFiles: [{ source: workerPath, destination: 'ngsw-worker.js' }], + }; + + // If present, write the safety worker code + const safetyPath = path.join(path.dirname(workerPath), 'safety-worker.js'); + if (existsSync(safetyPath)) { + result.assetFiles.push({ source: safetyPath, destination: 'worker-basic.min.js' }); + result.assetFiles.push({ source: safetyPath, destination: 'safety-worker.js' }); + } + + return result; +} diff --git a/packages/angular/build/src/utils/stats-table.ts b/packages/angular/build/src/utils/stats-table.ts new file mode 100644 index 000000000000..b007fd7a4aa5 --- /dev/null +++ b/packages/angular/build/src/utils/stats-table.ts @@ -0,0 +1,290 @@ +/** + * @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 { stripVTControlCharacters } from 'node:util'; +import { BudgetCalculatorResult } from './bundle-calculator'; +import { colors as ansiColors } from './color'; +import { formatSize } from './format-bytes'; + +export type BundleStatsData = [ + files: string, + names: string, + rawSize: number | string, + estimatedTransferSize: number | string, +]; +export interface BundleStats { + initial: boolean; + stats: BundleStatsData; +} + +export function generateEsbuildBuildStatsTable( + [browserStats, serverStats]: [browserStats: BundleStats[], serverStats: BundleStats[]], + colors: boolean, + showTotalSize: boolean, + showEstimatedTransferSize: boolean, + budgetFailures?: BudgetCalculatorResult[], + verbose?: boolean, +): string { + const bundleInfo = generateBuildStatsData( + browserStats, + colors, + showTotalSize, + showEstimatedTransferSize, + budgetFailures, + verbose, + ); + + if (serverStats.length) { + const m = (x: string) => (colors ? ansiColors.magenta(x) : x); + if (browserStats.length) { + bundleInfo.unshift([m('Browser bundles')]); + // Add seperators between browser and server logs + bundleInfo.push([], []); + } + + bundleInfo.push( + [m('Server bundles')], + ...generateBuildStatsData(serverStats, colors, false, false, undefined, verbose), + ); + } + + return generateTableText(bundleInfo, colors); +} + +export function generateBuildStatsTable( + data: BundleStats[], + colors: boolean, + showTotalSize: boolean, + showEstimatedTransferSize: boolean, + budgetFailures?: BudgetCalculatorResult[], +): string { + const bundleInfo = generateBuildStatsData( + data, + colors, + showTotalSize, + showEstimatedTransferSize, + budgetFailures, + true, + ); + + return generateTableText(bundleInfo, colors); +} + +function generateBuildStatsData( + data: BundleStats[], + colors: boolean, + showTotalSize: boolean, + showEstimatedTransferSize: boolean, + budgetFailures?: BudgetCalculatorResult[], + verbose?: boolean, +): (string | number)[][] { + if (data.length === 0) { + return []; + } + + const g = (x: string) => (colors ? ansiColors.green(x) : x); + const c = (x: string) => (colors ? ansiColors.cyan(x) : x); + const r = (x: string) => (colors ? ansiColors.redBright(x) : x); + const y = (x: string) => (colors ? ansiColors.yellowBright(x) : x); + const bold = (x: string) => (colors ? ansiColors.bold(x) : x); + const dim = (x: string) => (colors ? ansiColors.dim(x) : x); + + const getSizeColor = (name: string, file?: string, defaultColor = c) => { + const severity = budgets.get(name) || (file && budgets.get(file)); + switch (severity) { + case 'warning': + return y; + case 'error': + return r; + default: + return defaultColor; + } + }; + + const changedEntryChunksStats: BundleStatsData[] = []; + const changedLazyChunksStats: BundleStatsData[] = []; + + let initialTotalRawSize = 0; + let changedLazyChunksCount = 0; + let initialTotalEstimatedTransferSize; + const maxLazyChunksWithoutBudgetFailures = 15; + + const budgets = new Map(); + if (budgetFailures) { + for (const { label, severity } of budgetFailures) { + // In some cases a file can have multiple budget failures. + // Favor error. + if (label && (!budgets.has(label) || budgets.get(label) === 'warning')) { + budgets.set(label, severity); + } + } + } + + // Sort descending by raw size + data.sort((a, b) => { + if (a.stats[2] > b.stats[2]) { + return -1; + } + + if (a.stats[2] < b.stats[2]) { + return 1; + } + + return 0; + }); + + for (const { initial, stats } of data) { + const [files, names, rawSize, estimatedTransferSize] = stats; + if ( + !initial && + !verbose && + changedLazyChunksStats.length >= maxLazyChunksWithoutBudgetFailures && + !budgets.has(names) && + !budgets.has(files) + ) { + // Limit the number of lazy chunks displayed in the stats table when there is no budget failure and not in verbose mode. + changedLazyChunksCount++; + continue; + } + + const getRawSizeColor = getSizeColor(names, files); + let data: BundleStatsData; + if (showEstimatedTransferSize) { + data = [ + g(files), + dim(names), + getRawSizeColor(typeof rawSize === 'number' ? formatSize(rawSize) : rawSize), + c( + typeof estimatedTransferSize === 'number' + ? formatSize(estimatedTransferSize) + : estimatedTransferSize, + ), + ]; + } else { + data = [ + g(files), + dim(names), + getRawSizeColor(typeof rawSize === 'number' ? formatSize(rawSize) : rawSize), + '', + ]; + } + + if (initial) { + changedEntryChunksStats.push(data); + if (typeof rawSize === 'number') { + initialTotalRawSize += rawSize; + } + if (showEstimatedTransferSize && typeof estimatedTransferSize === 'number') { + if (initialTotalEstimatedTransferSize === undefined) { + initialTotalEstimatedTransferSize = 0; + } + initialTotalEstimatedTransferSize += estimatedTransferSize; + } + } else { + changedLazyChunksStats.push(data); + changedLazyChunksCount++; + } + } + + const bundleInfo: (string | number)[][] = []; + const baseTitles = ['Names', 'Raw size']; + + if (showEstimatedTransferSize) { + baseTitles.push('Estimated transfer size'); + } + + // Entry chunks + if (changedEntryChunksStats.length) { + bundleInfo.push(['Initial chunk files', ...baseTitles].map(bold), ...changedEntryChunksStats); + + if (showTotalSize) { + const initialSizeTotalColor = getSizeColor('bundle initial', undefined, (x) => x); + const totalSizeElements = [ + ' ', + 'Initial total', + initialSizeTotalColor(formatSize(initialTotalRawSize)), + ]; + if (showEstimatedTransferSize) { + totalSizeElements.push( + typeof initialTotalEstimatedTransferSize === 'number' + ? formatSize(initialTotalEstimatedTransferSize) + : '-', + ); + } + bundleInfo.push([], totalSizeElements.map(bold)); + } + } + + // Seperator + if (changedEntryChunksStats.length && changedLazyChunksStats.length) { + bundleInfo.push([]); + } + + // Lazy chunks + if (changedLazyChunksStats.length) { + bundleInfo.push(['Lazy chunk files', ...baseTitles].map(bold), ...changedLazyChunksStats); + + if (changedLazyChunksCount > changedLazyChunksStats.length) { + bundleInfo.push([ + dim( + `...and ${changedLazyChunksCount - changedLazyChunksStats.length} more lazy chunks files. ` + + 'Use "--verbose" to show all the files.', + ), + ]); + } + } + + return bundleInfo; +} + +function generateTableText(bundleInfo: (string | number)[][], colors: boolean): string { + const skipText = (value: string) => value.includes('...and '); + const longest: number[] = []; + for (const item of bundleInfo) { + for (let i = 0; i < item.length; i++) { + if (item[i] === undefined) { + continue; + } + + const currentItem = item[i].toString(); + if (skipText(currentItem)) { + continue; + } + + const currentLongest = (longest[i] ??= 0); + const currentItemLength = stripVTControlCharacters(currentItem).length; + if (currentLongest < currentItemLength) { + longest[i] = currentItemLength; + } + } + } + + const seperator = colors ? ansiColors.dim(' | ') : ' | '; + const outputTable: string[] = []; + for (const item of bundleInfo) { + for (let i = 0; i < longest.length; i++) { + if (item[i] === undefined) { + continue; + } + + const currentItem = item[i].toString(); + if (skipText(currentItem)) { + continue; + } + + const currentItemLength = stripVTControlCharacters(currentItem).length; + const stringPad = ' '.repeat(longest[i] - currentItemLength); + // Values in columns at index 2 and 3 (Raw and Estimated sizes) are always right aligned. + item[i] = i >= 2 ? stringPad + currentItem : currentItem + stringPad; + } + + outputTable.push(item.join(seperator)); + } + + return outputTable.join('\n'); +} diff --git a/packages/angular/build/src/utils/supported-browsers.ts b/packages/angular/build/src/utils/supported-browsers.ts new file mode 100644 index 000000000000..a8546a039082 --- /dev/null +++ b/packages/angular/build/src/utils/supported-browsers.ts @@ -0,0 +1,62 @@ +/** + * @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 browserslist from 'browserslist'; + +export function getSupportedBrowsers( + projectRoot: string, + logger: { warn(message: string): void }, +): string[] { + // Read the browserslist configuration containing Angular's browser support policy. + const angularBrowserslist = browserslist(undefined, { + path: require.resolve('../../.browserslistrc'), + }); + + // Use Angular's configuration as the default. + browserslist.defaults = angularBrowserslist; + + // Get the minimum set of browser versions supported by Angular. + const minimumBrowsers = new Set(angularBrowserslist); + + // Get browsers from config or default. + const browsersFromConfigOrDefault = new Set(browserslist(undefined, { path: projectRoot })); + + // Get browsers that support ES6 modules. + const browsersThatSupportEs6 = new Set(browserslist('supports es6-module')); + + const nonEs6Browsers: string[] = []; + const unsupportedBrowsers: string[] = []; + for (const browser of browsersFromConfigOrDefault) { + if (!browsersThatSupportEs6.has(browser)) { + // Any browser which does not support ES6 is explicitly ignored, as Angular will not build successfully. + browsersFromConfigOrDefault.delete(browser); + nonEs6Browsers.push(browser); + } else if (!minimumBrowsers.has(browser)) { + // Any other unsupported browser we will attempt to use, but provide no support for. + unsupportedBrowsers.push(browser); + } + } + + if (nonEs6Browsers.length) { + logger.warn( + `One or more browsers which are configured in the project's Browserslist configuration ` + + 'will be ignored as ES5 output is not supported by the Angular CLI.\n' + + `Ignored browsers:\n${nonEs6Browsers.join(', ')}`, + ); + } + + if (unsupportedBrowsers.length) { + logger.warn( + `One or more browsers which are configured in the project's Browserslist configuration ` + + "fall outside Angular's browser support for this version.\n" + + `Unsupported browsers:\n${unsupportedBrowsers.join(', ')}`, + ); + } + + return Array.from(browsersFromConfigOrDefault); +} diff --git a/packages/angular/build/src/utils/tty.ts b/packages/angular/build/src/utils/tty.ts new file mode 100644 index 000000000000..0d669c0301e3 --- /dev/null +++ b/packages/angular/build/src/utils/tty.ts @@ -0,0 +1,22 @@ +/** + * @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 + */ + +function _isTruthy(value: undefined | string): boolean { + // Returns true if value is a string that is anything but 0 or false. + return value !== undefined && value !== '0' && value.toUpperCase() !== 'FALSE'; +} + +export function isTTY(): boolean { + // 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/packages/angular/build/src/utils/url.ts b/packages/angular/build/src/utils/url.ts new file mode 100644 index 000000000000..d3f1e5791276 --- /dev/null +++ b/packages/angular/build/src/utils/url.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 function urlJoin(...parts: string[]): string { + const [p, ...rest] = parts; + + // Remove trailing slash from first part + // Join all parts with `/` + // Dedupe double slashes from path names + return p.replace(/\/$/, '') + ('/' + rest.join('/')).replace(/\/\/+/g, '/'); +} diff --git a/packages/angular/build/src/utils/version.ts b/packages/angular/build/src/utils/version.ts new file mode 100644 index 000000000000..51f493bfe993 --- /dev/null +++ b/packages/angular/build/src/utils/version.ts @@ -0,0 +1,57 @@ +/** + * @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 + */ + +/* eslint-disable no-console */ + +import { createRequire } from 'node:module'; +import { SemVer, satisfies } from 'semver'; + +export function assertCompatibleAngularVersion(projectRoot: string): void | never { + let angularPkgJson; + + // Create a custom require function for ESM compliance. + // NOTE: The trailing slash is significant. + const projectRequire = createRequire(projectRoot + '/'); + + try { + angularPkgJson = projectRequire('@angular/core/package.json'); + } catch { + console.error( + 'Error: It appears that "@angular/core" is missing as a dependency. Please ensure it is included in your project.', + ); + + process.exit(2); + } + + if (!angularPkgJson?.['version']) { + console.error( + 'Error: Unable to determine the versions of "@angular/core".\n' + + 'This likely indicates a corrupted local installation. Please try reinstalling your packages.', + ); + + process.exit(2); + } + + const supportedAngularSemver = '0.0.0-ANGULAR-FW-PEER-DEP'; + if (angularPkgJson['version'] === '0.0.0' || supportedAngularSemver.startsWith('0.0.0')) { + // Internal CLI and FW testing version. + return; + } + + const angularVersion = new SemVer(angularPkgJson['version']); + + if (!satisfies(angularVersion, supportedAngularSemver, { includePrerelease: true })) { + console.error( + `Error: The current version of "@angular/build" supports Angular versions ${supportedAngularSemver},\n` + + `but detected Angular version ${angularVersion} instead.\n` + + 'Please visit the link below to find instructions on how to update Angular.\nhttps://update.angular.dev/', + ); + + process.exit(3); + } +} diff --git a/packages/angular/build/src/utils/worker-pool.ts b/packages/angular/build/src/utils/worker-pool.ts new file mode 100644 index 000000000000..3a4b3def27cb --- /dev/null +++ b/packages/angular/build/src/utils/worker-pool.ts @@ -0,0 +1,44 @@ +/** + * @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 { getCompileCacheDir } from 'node:module'; +import { Piscina } from 'piscina'; + +export type WorkerPoolOptions = ConstructorParameters[0]; + +export class WorkerPool extends Piscina { + constructor(options: WorkerPoolOptions) { + const piscinaOptions: WorkerPoolOptions = { + minThreads: 1, + idleTimeout: 1000, + // Web containers do not support transferable objects with receiveOnMessagePort which + // is used when the Atomics based wait loop is enable. + atomics: process.versions.webcontainer ? 'disabled' : 'sync', + recordTiming: false, + ...options, + }; + + // Enable compile code caching if enabled for the main process (only exists on Node.js v22.8+). + // Skip if running inside Bazel via a RUNFILES environment variable check. The cache does not work + // well with Bazel's hermeticity requirements. + const compileCacheDirectory = process.env['RUNFILES'] ? undefined : getCompileCacheDir?.(); + if (compileCacheDirectory) { + if (typeof piscinaOptions.env === 'object') { + piscinaOptions.env['NODE_COMPILE_CACHE'] = compileCacheDirectory; + } else { + // Default behavior of `env` option is to copy current process values + piscinaOptions.env = { + ...process.env, + 'NODE_COMPILE_CACHE': compileCacheDirectory, + }; + } + } + + super(piscinaOptions); + } +} diff --git a/packages/angular/cli/BUILD b/packages/angular/cli/BUILD deleted file mode 100644 index 19d60b27d639..000000000000 --- a/packages/angular/cli/BUILD +++ /dev/null @@ -1,33 +0,0 @@ -# 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 - -licenses(["notice"]) # MIT - -load("//tools:defaults.bzl", "ts_library") - -package(default_visibility = ["//visibility:public"]) - -ts_library( - name = "angular-cli", - srcs = glob( - ["**/*.ts"], - exclude = [ - "**/*_spec.ts", - "**/*_spec_large.ts", - ], - ), - deps = [ - "//packages/angular_devkit/architect", - "//packages/angular_devkit/core", - "//packages/angular_devkit/core:node", - "//packages/angular_devkit/schematics", - "//packages/angular_devkit/schematics:tools", - "@rxjs", - "@rxjs//operators", - # @typings: node - # @typings: semver - ], - tsconfig = "//:tsconfig.json" -) \ No newline at end of file diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel new file mode 100644 index 000000000000..efa8d5f601f0 --- /dev/null +++ b/packages/angular/cli/BUILD.bazel @@ -0,0 +1,150 @@ +# 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.dev/license + +load("@npm//:defs.bzl", "npm_link_all_packages") +load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project") +load("//tools:ng_cli_schema_generator.bzl", "cli_json_schema") +load("//tools:ts_json_schema.bzl", "ts_json_schema") + +licenses(["notice"]) + +package(default_visibility = ["//visibility:public"]) + +npm_link_all_packages() + +RUNTIME_ASSETS = glob( + include = [ + "bin/**/*", + "src/**/*.md", + "src/**/*.json", + ], + exclude = [ + "lib/config/workspace-schema.json", + ], +) + [ + "//packages/angular/cli:lib/config/schema.json", +] + +ts_project( + name = "angular-cli", + srcs = glob( + include = [ + "lib/**/*.ts", + "src/**/*.ts", + ], + exclude = [ + "**/*_spec.ts", + ], + ) + [ + # These files are generated from the JSON schema + "//packages/angular/cli:lib/config/workspace-schema.ts", + "//packages/angular/cli:src/commands/update/schematic/schema.ts", + ], + data = RUNTIME_ASSETS, + deps = [ + ":node_modules/@angular-devkit/architect", + ":node_modules/@angular-devkit/core", + ":node_modules/@angular-devkit/schematics", + ":node_modules/@inquirer/prompts", + ":node_modules/@listr2/prompt-adapter-inquirer", + ":node_modules/@yarnpkg/lockfile", + ":node_modules/ini", + ":node_modules/jsonc-parser", + ":node_modules/npm-package-arg", + ":node_modules/npm-pick-manifest", + ":node_modules/pacote", + ":node_modules/resolve", + ":node_modules/yargs", + "//:node_modules/@angular/core", + "//:node_modules/@types/ini", + "//:node_modules/@types/node", + "//:node_modules/@types/npm-package-arg", + "//:node_modules/@types/pacote", + "//:node_modules/@types/resolve", + "//:node_modules/@types/semver", + "//:node_modules/@types/yargs", + "//:node_modules/@types/yarnpkg__lockfile", + "//:node_modules/listr2", + "//:node_modules/semver", + ], +) + +CLI_SCHEMA_DATA = [ + "//packages/angular/build:schemas", + "//packages/angular_devkit/build_angular:schemas", + "//packages/schematics/angular:schemas", +] + +cli_json_schema( + name = "cli_config_schema", + src = "lib/config/workspace-schema.json", + out = "lib/config/schema.json", + data = CLI_SCHEMA_DATA, +) + +ts_json_schema( + name = "cli_schema", + src = "lib/config/workspace-schema.json", + data = CLI_SCHEMA_DATA, +) + +ts_json_schema( + name = "update_schematic_schema", + src = "src/commands/update/schematic/schema.json", +) + +ts_project( + name = "angular-cli_test_lib", + testonly = True, + srcs = glob( + include = ["**/*_spec.ts"], + exclude = [ + # NB: we need to exclude the nested node_modules that is laid out by yarn workspaces + "node_modules/**", + ], + ), + deps = [ + ":angular-cli", + ":node_modules/@angular-devkit/core", + ":node_modules/@angular-devkit/schematics", + ":node_modules/yargs", + "//:node_modules/@types/semver", + "//:node_modules/@types/yargs", + "//:node_modules/semver", + ], +) + +jasmine_test( + name = "angular-cli_test", + data = [":angular-cli_test_lib"], +) + +genrule( + name = "license", + srcs = ["//:LICENSE"], + outs = ["LICENSE"], + cmd = "cp $(execpath //:LICENSE) $@", +) + +npm_package( + name = "pkg", + pkg_deps = [ + "//packages/angular_devkit/architect:package.json", + "//packages/angular_devkit/build_angular:package.json", + "//packages/angular_devkit/build_webpack:package.json", + "//packages/angular_devkit/core:package.json", + "//packages/angular_devkit/schematics:package.json", + "//packages/schematics/angular:package.json", + ], + stamp_files = [ + "src/utilities/version.js", + ], + tags = ["release-package"], + deps = RUNTIME_ASSETS + [ + ":README.md", + ":angular-cli", + ":license", + ], +) diff --git a/packages/angular/cli/README.md b/packages/angular/cli/README.md index f88010a3f0e5..4fa87391f04c 100644 --- a/packages/angular/cli/README.md +++ b/packages/angular/cli/README.md @@ -1,205 +1,5 @@ -## Angular CLI +# Angular CLI - The CLI tool for Angular. - -[![Dependency Status][david-badge]][david-badge-url] -[![devDependency Status][david-dev-badge]][david-dev-badge-url] - -[![npm](https://img.shields.io/npm/v/%40angular/cli.svg)][npm-badge-url] -[![npm](https://img.shields.io/npm/v/%40angular/cli/next.svg)][npm-badge-url] -[![npm](https://img.shields.io/npm/l/@angular/cli.svg)][npm-badge-url] -[![npm](https://img.shields.io/npm/dm/@angular/cli.svg)][npm-badge-url] - -[![Join the chat at https://gitter.im/angular/angular-cli](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/angular/angular-cli?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -[![GitHub forks](https://img.shields.io/github/forks/angular/angular-cli.svg?style=social&label=Fork)](https://github.com/angular/angular-cli/fork) -[![GitHub stars](https://img.shields.io/github/stars/angular/angular-cli.svg?style=social&label=Star)](https://github.com/angular/angular-cli) - - -## Note - -If you are updating from a beta or RC version, check out our [1.0 Update Guide](https://github.com/angular/angular-cli/wiki/stories-1.0-update). - -If you wish to collaborate, check out [our issue list](https://github.com/angular/angular-cli/issues). - -Before submitting new issues, have a look at [issues marked with the `type: faq` label](https://github.com/angular/angular-cli/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3A%22type%3A%20faq%22%20). - -## Prerequisites - -Both the CLI and generated project have dependencies that require Node 8.9 or higher, together -with NPM 5.5.1 or higher. - -## Table of Contents - -* [Installation](#installation) -* [Usage](#usage) -* [Generating a New Project](#generating-and-serving-an-angular-project-via-a-development-server) -* [Generating Components, Directives, Pipes and Services](#generating-components-directives-pipes-and-services) -* [Updating Angular CLI](#updating-angular-cli) -* [Development Hints for working on Angular CLI](#development-hints-for-working-on-angular-cli) -* [Documentation](#documentation) -* [License](#license) - -## Installation - -**BEFORE YOU INSTALL:** please read the [prerequisites](#prerequisites) -```bash -npm install -g @angular/cli -``` - -## Usage - -```bash -ng help -``` - -### Generating and serving an Angular project via a development server - -```bash -ng new PROJECT-NAME -cd PROJECT-NAME -ng serve -``` -Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. - -You can configure the default HTTP host and port used by the development server with two command-line options : - -```bash -ng serve --host 0.0.0.0 --port 4201 -``` - -### Generating Components, Directives, Pipes and Services - -You can use the `ng generate` (or just `ng g`) command to generate Angular components: - -```bash -ng generate component my-new-component -ng g component my-new-component # using the alias - -# components support relative path generation -# if in the directory src/app/feature/ and you run -ng g component new-cmp -# your component will be generated in src/app/feature/new-cmp -# but if you were to run -ng g component ./newer-cmp -# your component will be generated in src/app/newer-cmp -# if in the directory src/app you can also run -ng g component feature/new-cmp -# and your component will be generated in src/app/feature/new-cmp -``` -You can find all possible blueprints in the table below: - -Scaffold | Usage ---- | --- -[Component](https://github.com/angular/angular-cli/wiki/generate-component) | `ng g component my-new-component` -[Directive](https://github.com/angular/angular-cli/wiki/generate-directive) | `ng g directive my-new-directive` -[Pipe](https://github.com/angular/angular-cli/wiki/generate-pipe) | `ng g pipe my-new-pipe` -[Service](https://github.com/angular/angular-cli/wiki/generate-service) | `ng g service my-new-service` -[Class](https://github.com/angular/angular-cli/wiki/generate-class) | `ng g class my-new-class` -[Guard](https://github.com/angular/angular-cli/wiki/generate-guard) | `ng g guard my-new-guard` -[Interface](https://github.com/angular/angular-cli/wiki/generate-interface) | `ng g interface my-new-interface` -[Enum](https://github.com/angular/angular-cli/wiki/generate-enum) | `ng g enum my-new-enum` -[Module](https://github.com/angular/angular-cli/wiki/generate-module) | `ng g module my-module` - - - - -angular-cli will add reference to `components`, `directives` and `pipes` automatically in the `app.module.ts`. If you need to add this references to another custom module, follow this steps: - - 1. `ng g module new-module` to create a new module - 2. call `ng g component new-module/new-component` - -This should add the new `component`, `directive` or `pipe` reference to the `new-module` you've created. - -### Updating Angular CLI - -If you're using Angular CLI `1.0.0-beta.28` or less, you need to uninstall `angular-cli` package. It should be done due to changing of package's name and scope from `angular-cli` to `@angular/cli`: -```bash -npm uninstall -g angular-cli -npm uninstall --save-dev angular-cli -``` - -To update Angular CLI to a new version, you must update both the global package and your project's local package. - -Global package: -```bash -npm uninstall -g @angular/cli -npm cache verify -# if npm version is < 5 then use `npm cache clean` -npm install -g @angular/cli@latest -``` - -Local project package: -```bash -rm -rf node_modules dist # use rmdir /S/Q node_modules dist in Windows Command Prompt; use rm -r -fo node_modules,dist in Windows PowerShell -npm install --save-dev @angular/cli@latest -npm install -``` - -If you are updating to 1.0 from a beta or RC version, check out our [1.0 Update Guide](https://github.com/angular/angular-cli/wiki/stories-1.0-update). - -You can find more details about changes between versions in [the Releases tab on GitHub](https://github.com/angular/angular-cli/releases). - - -## Development Hints for working on Angular CLI - -### Working with master - -```bash -git clone https://github.com/angular/angular-cli.git -cd angular-cli -npm link -``` - -`npm link` is very similar to `npm install -g` except that instead of downloading the package -from the repo, the just cloned `angular-cli/` folder becomes the global package. -Additionally, this repository publishes several packages and we use special logic to load all of them -on development setups. - -Any changes to the files in the `angular-cli/` folder will immediately affect the global `@angular/cli` package, -allowing you to quickly test any changes you make to the cli project. - -Now you can use `@angular/cli` via the command line: - -```bash -ng new foo -cd foo -npm link @angular/cli -ng serve -``` - -`npm link @angular/cli` is needed because by default the globally installed `@angular/cli` just loads -the local `@angular/cli` from the project which was fetched remotely from npm. -`npm link @angular/cli` symlinks the global `@angular/cli` package to the local `@angular/cli` package. -Now the `angular-cli` you cloned before is in three places: -The folder you cloned it into, npm's folder where it stores global packages and the Angular CLI project you just created. - -You can also use `ng new foo --link-cli` to automatically link the `@angular/cli` package. - -Please read the official [npm-link documentation](https://docs.npmjs.com/cli/link) -and the [npm-link cheatsheet](http://browsenpm.org/help#linkinganynpmpackagelocally) for more information. - -To run the Angular CLI test suite use the `node tests/run_e2e.js` command. -It can also receive a filename to only run that test (e.g. `node tests/run_e2e.js tests/e2e/tests/build/dev-build.ts`). - -As part of the test procedure, all packages will be built and linked. -You will need to re-run `npm link` to re-link the development Angular CLI environment after tests finish. - - -## Documentation - -The documentation for the Angular CLI is located in this repo's [wiki](https://github.com/angular/angular-cli/wiki). - -## License - -MIT - - -[travis-badge]: https://travis-ci.org/angular/angular-cli.svg?branch=master -[travis-badge-url]: https://travis-ci.org/angular/angular-cli -[david-badge]: https://david-dm.org/angular/angular-cli.svg -[david-badge-url]: https://david-dm.org/angular/angular-cli -[david-dev-badge]: https://david-dm.org/angular/angular-cli/dev-status.svg -[david-dev-badge-url]: https://david-dm.org/angular/angular-cli?type=dev -[npm-badge]: https://img.shields.io/npm/v/@angular/cli.svg -[npm-badge-url]: https://www.npmjs.com/package/@angular/cli +The sources for this package are in the [Angular CLI](https://github.com/angular/angular-cli) repository. Please file issues and pull requests against that repository. +Usage information and reference details can be found in repository [README](https://github.com/angular/angular-cli/blob/main/README.md) file. diff --git a/packages/angular/cli/bin/bootstrap.js b/packages/angular/cli/bin/bootstrap.js new file mode 100644 index 000000000000..1282f906aef2 --- /dev/null +++ b/packages/angular/cli/bin/bootstrap.js @@ -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 + */ + +/** + * @fileoverview + * + * This file is used to bootstrap the CLI process by dynamically importing the main initialization code. + * This is done to allow the main bin file (`ng`) to remain CommonJS so that older versions of Node.js + * can be checked and validated prior to the execution of the CLI. This separate bootstrap file is + * needed to allow the use of a dynamic import expression without crashing older versions of Node.js that + * do not support dynamic import expressions and would otherwise throw a syntax error. This bootstrap file + * is required from the main bin file only after the Node.js version is determined to be in the supported + * range. + */ + +// Enable on-disk code caching if available (Node.js 22.8+) +// Skip if running inside Bazel via a RUNFILES environment variable check. The cache does not work +// well with Bazel's hermeticity requirements. +if (!process.env['RUNFILES']) { + try { + const { enableCompileCache } = require('node:module'); + + enableCompileCache?.(); + } catch {} +} + +// Initialize the Angular CLI +void import('../lib/init.js'); diff --git a/packages/angular/cli/bin/ng b/packages/angular/cli/bin/ng deleted file mode 100755 index 6114439119ad..000000000000 --- a/packages/angular/cli/bin/ng +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Provide a title to the process in `ps`. -// Due to an obscure Mac bug, do not start this title with any symbol. -process.title = 'ng'; - -var version = process.version.substr(1).split('.'); -if (Number(version[0]) < 8 || (Number(version[0]) === 8 && Number(version[1]) < 9)) { - process.stderr.write( - 'You are running version ' + process.version + ' of Node.js, which is not supported by Angular CLI v6.\n' + - 'The official Node.js version that is supported is 8.9 and greater.\n\n' + - 'Please visit https://nodejs.org/en/ to find instructions on how to update Node.js.\n' - ); - - process.exit(3); -} - -require('../lib/init'); diff --git a/packages/angular/cli/bin/ng-update-message.js b/packages/angular/cli/bin/ng-update-message.js deleted file mode 100755 index 81e161911116..000000000000 --- a/packages/angular/cli/bin/ng-update-message.js +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Check if the current directory contains '.angular-cli.json'. If it does, show a message to the user that they -// should use the migration script. - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - - -let found = false; -let current = path.dirname(path.dirname(__dirname)); -while (current !== path.dirname(current)) { - if (fs.existsSync(path.join(current, 'angular-cli.json')) - || fs.existsSync(path.join(current, '.angular-cli.json'))) { - found = os.homedir() !== current || fs.existsSync(path.join(current, 'package.json')); - break; - } - if (fs.existsSync(path.join(current, 'angular.json')) - || fs.existsSync(path.join(current, '.angular.json')) - || fs.existsSync(path.join(current, 'package.json'))) { - break; - } - - current = path.dirname(current); -} - - -if (found) { - // ------------------------------------------------------------------------------------------ - // If changing this message, please update the same message in - // `packages/@angular/cli/models/command-runner.ts` - - // eslint-disable-next-line no-console - console.error(`\u001b[31m - ${'='.repeat(80)} - The Angular CLI configuration format has been changed, and your existing configuration can - be updated automatically by running the following command: - - ng update @angular/cli - ${'='.repeat(80)} - \u001b[39m`.replace(/^ {4}/gm, '')); -} diff --git a/packages/angular/cli/bin/ng.js b/packages/angular/cli/bin/ng.js new file mode 100755 index 000000000000..392578c684cb --- /dev/null +++ b/packages/angular/cli/bin/ng.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node +/** + * @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 + */ + +/* eslint-disable no-console */ +/* eslint-disable import/no-unassigned-import */ +'use strict'; + +const path = require('path'); + +// Error if the external CLI appears to be used inside a google3 context. +if (process.cwd().split(path.sep).includes('google3')) { + console.error( + 'This is the external Angular CLI, but you appear to be running in google3. There is a separate, internal version of the CLI which should be used instead. See http://go/angular/cli.', + ); + process.exit(); +} + +// Provide a title to the process in `ps`. +// Due to an obscure Mac bug, do not start this title with any symbol. +try { + process.title = 'ng ' + Array.from(process.argv).slice(2).join(' '); +} catch (_) { + // If an error happened above, use the most basic title. + process.title = 'ng'; +} + +const rawCommandName = process.argv[2]; + +if (rawCommandName === '--get-yargs-completions' || rawCommandName === 'completion') { + // Skip Node.js supported checks when running ng completion. + // A warning at this stage could cause a broken source action (`source <(ng completion script)`) when in the shell init script. + require('./bootstrap'); + + return; +} + +// This node version check ensures that extremely old versions of node are not used. +// These may not support ES2015 features such as const/let/async/await/etc. +// These would then crash with a hard to diagnose error message. +var version = process.versions.node.split('.').map((part) => Number(part)); +if (version[0] % 2 === 1) { + // Allow new odd numbered releases with a warning (currently v17+) + console.warn( + 'Node.js version ' + + process.version + + ' detected.\n' + + 'Odd numbered Node.js versions will not enter LTS status and should not be used for production.' + + ' For more information, please see https://nodejs.org/en/about/previous-releases/.', + ); + + require('./bootstrap'); +} else if (version[0] < 20 || (version[0] === 20 && version[1] < 11)) { + // Error and exit if less than 20.11 + console.error( + 'Node.js version ' + + process.version + + ' detected.\n' + + 'The Angular CLI requires a minimum Node.js version of v20.11.\n\n' + + 'Please update your Node.js version or visit https://nodejs.org/ for additional instructions.\n', + ); + + process.exitCode = 3; +} else { + require('./bootstrap'); +} diff --git a/packages/angular/cli/bin/package.json b/packages/angular/cli/bin/package.json new file mode 100644 index 000000000000..5bbefffbabee --- /dev/null +++ b/packages/angular/cli/bin/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/packages/angular/cli/commands/add.ts b/packages/angular/cli/commands/add.ts deleted file mode 100644 index 445524b07cf4..000000000000 --- a/packages/angular/cli/commands/add.ts +++ /dev/null @@ -1,119 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { tags, terminal } from '@angular-devkit/core'; -import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools'; -import { CommandScope, Option } from '../models/command'; -import { parseOptions } from '../models/command-runner'; -import { SchematicCommand } from '../models/schematic-command'; -import { NpmInstall } from '../tasks/npm-install'; -import { getPackageManager } from '../utilities/config'; - - -export class AddCommand extends SchematicCommand { - readonly name = 'add'; - readonly description = 'Add support for a library to your project.'; - readonly allowPrivateSchematics = true; - static aliases = []; - static scope = CommandScope.inProject; - arguments = ['collection']; - options: Option[] = []; - - private async _parseSchematicOptions(collectionName: string): Promise { - const schematicOptions = await this.getOptions({ - schematicName: 'ng-add', - collectionName, - }); - - const options = this.options.concat(schematicOptions.options); - const args = schematicOptions.arguments.map(arg => arg.name); - - return parseOptions(this._rawArgs, options, args, this.argStrategy); - } - - validate(options: any) { - const collectionName = options._[0]; - - if (!collectionName) { - this.logger.fatal( - `The "ng ${this.name}" command requires a name argument to be specified eg. ` - + `${terminal.yellow('ng add [name] ')}. For more details, use "ng help".`, - ); - - return false; - } - - return true; - } - - async run(options: any) { - const firstArg = options._[0]; - - if (!firstArg) { - this.logger.fatal( - `The "ng ${this.name}" command requires a name argument to be specified eg. ` - + `${terminal.yellow('ng add [name] ')}. For more details, use "ng help".`, - ); - - return 1; - } - - const packageManager = getPackageManager(); - - const npmInstall: NpmInstall = require('../tasks/npm-install').default; - - const packageName = firstArg.startsWith('@') - ? firstArg.split('/', 2).join('/') - : firstArg.split('/', 1)[0]; - - // Remove the tag/version from the package name. - const collectionName = ( - packageName.startsWith('@') - ? packageName.split('@', 2).join('@') - : packageName.split('@', 1).join('@') - ) + firstArg.slice(packageName.length); - - // We don't actually add the package to package.json, that would be the work of the package - // itself. - await npmInstall( - packageName, - this.logger, - packageManager, - this.project.root, - ); - - // Reparse the options with the new schematic accessible. - options = await this._parseSchematicOptions(collectionName); - - const runOptions = { - schematicOptions: options, - workingDir: this.project.root, - collectionName, - schematicName: 'ng-add', - allowPrivate: true, - dryRun: false, - force: false, - }; - - try { - return await this.runSchematic(runOptions); - } catch (e) { - if (e instanceof NodePackageDoesNotSupportSchematics) { - this.logger.error(tags.oneLine` - The package that you are trying to add does not support schematics. You can try using - a different version of the package or contact the package author to add ng-add support. - `); - - return 1; - } - - throw e; - } - } -} diff --git a/packages/angular/cli/commands/build.ts b/packages/angular/cli/commands/build.ts deleted file mode 100644 index 6ce1a82f9a77..000000000000 --- a/packages/angular/cli/commands/build.ts +++ /dev/null @@ -1,36 +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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { CommandScope, Option } from '../models/command'; -import { Version } from '../upgrade/version'; - -export class BuildCommand extends ArchitectCommand { - public readonly name = 'build'; - public readonly target = 'build'; - public readonly description = - 'Builds your app and places it into the output path (dist/ by default).'; - public static aliases = ['b']; - public static scope = CommandScope.inProject; - public options: Option[] = [ - this.prodOption, - this.configurationOption, - ]; - - public validate(options: ArchitectCommandOptions) { - // Check Angular and TypeScript versions. - Version.assertCompatibleAngularVersion(this.project.root); - Version.assertTypescriptVersion(this.project.root); - - return super.validate(options); - } - - public async run(options: ArchitectCommandOptions) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/config.ts b/packages/angular/cli/commands/config.ts deleted file mode 100644 index 3034dbe92d52..000000000000 --- a/packages/angular/cli/commands/config.ts +++ /dev/null @@ -1,286 +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 { - InvalidJsonCharacterException, - JsonArray, - JsonObject, - JsonParseMode, - JsonValue, - experimental, - parseJson, - tags, -} from '@angular-devkit/core'; -import { writeFileSync } from 'fs'; -import { Command, Option } from '../models/command'; -import { - getWorkspace, - getWorkspaceRaw, - migrateLegacyGlobalConfig, - validateWorkspace, -} from '../utilities/config'; - - -export interface ConfigOptions { - jsonPath: string; - value?: string; - global?: boolean; -} - -const validCliPaths = new Map([ - ['cli.warnings.versionMismatch', 'boolean'], - ['cli.warnings.typescriptMismatch', 'boolean'], - ['cli.defaultCollection', 'string'], - ['cli.packageManager', 'string'], -]); - -/** - * Splits a JSON path string into fragments. Fragments can be used to get the value referenced - * by the path. For example, a path of "a[3].foo.bar[2]" would give you a fragment array of - * ["a", 3, "foo", "bar", 2]. - * @param path The JSON string to parse. - * @returns {string[]} The fragments for the string. - * @private - */ -function parseJsonPath(path: string): string[] { - const fragments = (path || '').split(/\./g); - const result: string[] = []; - - while (fragments.length > 0) { - const fragment = fragments.shift(); - if (fragment == undefined) { - break; - } - - const match = fragment.match(/([^\[]+)((\[.*\])*)/); - if (!match) { - throw new Error('Invalid JSON path.'); - } - - result.push(match[1]); - if (match[2]) { - const indices = match[2].slice(1, -1).split(']['); - result.push(...indices); - } - } - - return result.filter(fragment => !!fragment); -} - -function getValueFromPath( - root: T, - path: string, -): JsonValue | undefined { - const fragments = parseJsonPath(path); - - try { - return fragments.reduce((value: JsonValue, current: string | number) => { - if (value == undefined || typeof value != 'object') { - return undefined; - } else if (typeof current == 'string' && !Array.isArray(value)) { - return value[current]; - } else if (typeof current == 'number' && Array.isArray(value)) { - return value[current]; - } else { - return undefined; - } - }, root); - } catch { - return undefined; - } -} - -function setValueFromPath( - root: T, - path: string, - newValue: JsonValue, -): JsonValue | undefined { - const fragments = parseJsonPath(path); - - try { - return fragments.reduce((value: JsonValue, current: string | number, index: number) => { - if (value == undefined || typeof value != 'object') { - return undefined; - } else if (typeof current == 'string' && !Array.isArray(value)) { - if (index === fragments.length - 1) { - value[current] = newValue; - } else if (value[current] == undefined) { - if (typeof fragments[index + 1] == 'number') { - value[current] = []; - } else if (typeof fragments[index + 1] == 'string') { - value[current] = {}; - } - } - - return value[current]; - } else if (typeof current == 'number' && Array.isArray(value)) { - if (index === fragments.length - 1) { - value[current] = newValue; - } else if (value[current] == undefined) { - if (typeof fragments[index + 1] == 'number') { - value[current] = []; - } else if (typeof fragments[index + 1] == 'string') { - value[current] = {}; - } - } - - return value[current]; - } else { - return undefined; - } - }, root); - } catch { - return undefined; - } -} - -function normalizeValue(value: string, path: string): JsonValue { - const cliOptionType = validCliPaths.get(path); - if (cliOptionType) { - switch (cliOptionType) { - case 'boolean': - if (value.trim() === 'true') { - return true; - } else if (value.trim() === 'false') { - return false; - } - break; - case 'number': - const numberValue = Number(value); - if (!Number.isNaN(numberValue)) { - return numberValue; - } - break; - case 'string': - return value; - } - - throw new Error(`Invalid value type; expected a ${cliOptionType}.`); - } - - if (typeof value === 'string') { - try { - return parseJson(value, JsonParseMode.Loose); - } catch (e) { - if (e instanceof InvalidJsonCharacterException && !value.startsWith('{')) { - return value; - } else { - throw e; - } - } - } - - return value; -} - -export class ConfigCommand extends Command { - public readonly name = 'config'; - public readonly description = 'Get/set configuration values.'; - public readonly arguments = ['jsonPath', 'value']; - public readonly options: Option[] = [ - { - name: 'global', - type: Boolean, - 'default': false, - aliases: ['g'], - description: 'Get/set the value in the global configuration (in your home directory).', - }, - ]; - - public run(options: ConfigOptions) { - const level = options.global ? 'global' : 'local'; - - let config = - (getWorkspace(level) as {} as { _workspace: experimental.workspace.WorkspaceSchema }); - - if (options.global && !config) { - try { - if (migrateLegacyGlobalConfig()) { - config = - (getWorkspace(level) as {} as { _workspace: experimental.workspace.WorkspaceSchema }); - this.logger.info(tags.oneLine` - We found a global configuration that was used in Angular CLI 1. - It has been automatically migrated.`); - } - } catch {} - } - - if (options.value == undefined) { - if (!config) { - this.logger.error('No config found.'); - - return 1; - } - - return this.get(config._workspace, options); - } else { - return this.set(options); - } - } - - private get(config: experimental.workspace.WorkspaceSchema, options: ConfigOptions) { - let value; - if (options.jsonPath) { - value = getValueFromPath(config as {} as JsonObject, options.jsonPath); - } else { - value = config; - } - - if (value === undefined) { - this.logger.error('Value cannot be found.'); - - return 1; - } else if (typeof value == 'object') { - this.logger.info(JSON.stringify(value, null, 2)); - } else { - this.logger.info(value.toString()); - } - } - - private set(options: ConfigOptions) { - if (!options.jsonPath || !options.jsonPath.trim()) { - throw new Error('Invalid Path.'); - } - if (options.global - && !options.jsonPath.startsWith('schematics.') - && !validCliPaths.has(options.jsonPath)) { - throw new Error('Invalid Path.'); - } - - const [config, configPath] = getWorkspaceRaw(options.global ? 'global' : 'local'); - if (!config || !configPath) { - this.logger.error('Confguration file cannot be found.'); - - return 1; - } - - // TODO: Modify & save without destroying comments - const configValue = config.value; - - const value = normalizeValue(options.value || '', options.jsonPath); - const result = setValueFromPath(configValue, options.jsonPath, value); - - if (result === undefined) { - this.logger.error('Value cannot be found.'); - - return 1; - } - - try { - validateWorkspace(configValue); - } catch (error) { - this.logger.fatal(error.message); - - return 1; - } - - const output = JSON.stringify(configValue, null, 2); - writeFileSync(configPath, output); - } - -} diff --git a/packages/angular/cli/commands/doc.ts b/packages/angular/cli/commands/doc.ts deleted file mode 100644 index fe4dfe7ca3a7..000000000000 --- a/packages/angular/cli/commands/doc.ts +++ /dev/null @@ -1,50 +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 { Command } from '../models/command'; -const opn = require('opn'); - -export interface Options { - keyword: string; - search?: boolean; -} - -export class DocCommand extends Command { - public readonly name = 'doc'; - public readonly description = 'Opens the official Angular API documentation for a given keyword.'; - public static aliases = ['d']; - public readonly arguments = ['keyword']; - public readonly options = [ - { - name: 'search', - aliases: ['s'], - type: Boolean, - default: false, - description: 'Search whole angular.io instead of just api.', - }, - ]; - - public validate(options: Options) { - if (!options.keyword) { - this.logger.error(`keyword argument is required.`); - - return false; - } - - return true; - } - - public async run(options: Options) { - let searchUrl = `https://angular.io/api?query=${options.keyword}`; - if (options.search) { - searchUrl = `https://www.google.com/search?q=site%3Aangular.io+${options.keyword}`; - } - - return opn(searchUrl); - } -} diff --git a/packages/angular/cli/commands/e2e.ts b/packages/angular/cli/commands/e2e.ts deleted file mode 100644 index 3cc908f23ee5..000000000000 --- a/packages/angular/cli/commands/e2e.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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { CommandScope, Option } from '../models/command'; - - -export class E2eCommand extends ArchitectCommand { - public readonly name = 'e2e'; - public readonly target = 'e2e'; - public readonly description = 'Run e2e tests in existing project.'; - public static aliases: string[] = ['e']; - public static scope = CommandScope.inProject; - public readonly multiTarget = true; - public readonly options: Option[] = [ - this.prodOption, - this.configurationOption, - ]; - - public async run(options: ArchitectCommandOptions) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/easter-egg.ts b/packages/angular/cli/commands/easter-egg.ts deleted file mode 100644 index f6046ebd79b6..000000000000 --- a/packages/angular/cli/commands/easter-egg.ts +++ /dev/null @@ -1,36 +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 { terminal } from '@angular-devkit/core'; -import { Command, Option } from '../models/command'; - -function pickOne(of: string[]): string { - return of[Math.floor(Math.random() * of.length)]; -} - -export class AwesomeCommand extends Command { - public readonly name = 'make-this-awesome'; - public readonly description = ''; - public readonly hidden = true; - readonly arguments: string[] = []; - readonly options: Option[] = []; - - run() { - const phrase = pickOne([ - `You're on it, there's nothing for me to do!`, - `Let's take a look... nope, it's all good!`, - `You're doing fine.`, - `You're already doing great.`, - `Nothing to do; already awesome. Exiting.`, - `Error 418: As Awesome As Can Get.`, - `I spy with my little eye a great developer!`, - `Noop... already awesome.`, - ]); - this.logger.info(terminal.green(phrase)); - } -} diff --git a/packages/angular/cli/commands/eject.ts b/packages/angular/cli/commands/eject.ts deleted file mode 100644 index cf755606eca2..000000000000 --- a/packages/angular/cli/commands/eject.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 { tags } from '@angular-devkit/core'; -import { Command, Option } from '../models/command'; - - -export class EjectCommand extends Command { - public readonly name = 'eject'; - public readonly description = 'Temporarily disabled. Ejects your app and output the proper ' - + 'webpack configuration and scripts.'; - public readonly arguments: string[] = []; - public readonly options: Option[] = []; - public static aliases = []; - - run() { - this.logger.info(tags.stripIndents` - The 'eject' command has been temporarily disabled, as it is not yet compatible with the new - angular.json format. The new configuration format provides further flexibility to modify the - configuration of your workspace without ejecting. Ejection will be re-enabled in a future - release of the CLI. - - If you need to eject today, use CLI 1.7 to eject your project. - `); - } -} diff --git a/packages/angular/cli/commands/generate.ts b/packages/angular/cli/commands/generate.ts deleted file mode 100644 index 2b97327fbf10..000000000000 --- a/packages/angular/cli/commands/generate.ts +++ /dev/null @@ -1,119 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { tags, terminal } from '@angular-devkit/core'; -import { CommandScope, Option } from '../models/command'; -import { SchematicCommand } from '../models/schematic-command'; -import { getDefaultSchematicCollection } from '../utilities/config'; -import { - getCollection, - getEngineHost, -} from '../utilities/schematics'; - - -export class GenerateCommand extends SchematicCommand { - public readonly name = 'generate'; - public readonly description = 'Generates and/or modifies files based on a schematic.'; - public static aliases = ['g']; - public static scope = CommandScope.inProject; - public arguments = ['schematic']; - public options: Option[] = [ - ...this.coreOptions, - ]; - - private initialized = false; - public async initialize(options: any) { - if (this.initialized) { - return; - } - await super.initialize(options); - this.initialized = true; - - const [collectionName, schematicName] = this.parseSchematicInfo(options); - - if (!!schematicName) { - const schematicOptions = await this.getOptions({ - schematicName, - collectionName, - }); - this.options = this.options.concat(schematicOptions.options); - this.arguments = this.arguments.concat(schematicOptions.arguments.map(a => a.name)); - } - } - - validate(options: any): boolean | Promise { - if (!options._[0]) { - this.logger.error(tags.oneLine` - The "ng generate" command requires a - schematic name to be specified. - For more details, use "ng help".`); - - return false; - } - - return true; - } - - public run(options: any) { - const [collectionName, schematicName] = this.parseSchematicInfo(options); - - // remove the schematic name from the options - options._ = options._.slice(1); - - return this.runSchematic({ - collectionName, - schematicName, - schematicOptions: options, - debug: options.debug, - dryRun: options.dryRun, - force: options.force, - }); - } - - private parseSchematicInfo(options: any) { - let collectionName = getDefaultSchematicCollection(); - - let schematicName: string = options._[0]; - - if (schematicName) { - if (schematicName.includes(':')) { - [collectionName, schematicName] = schematicName.split(':', 2); - } - } - - return [collectionName, schematicName]; - } - - public printHelp(options: any) { - const schematicName = options._[0]; - if (schematicName) { - const argDisplay = this.arguments && this.arguments.length > 0 - ? ' ' + this.arguments.filter(a => a !== 'schematic').map(a => `<${a}>`).join(' ') - : ''; - const optionsDisplay = this.options && this.options.length > 0 - ? ' [options]' - : ''; - this.logger.info(`usage: ng generate ${schematicName}${argDisplay}${optionsDisplay}`); - this.printHelpOptions(options); - } else { - this.printHelpUsage(this.name, this.arguments, this.options); - const engineHost = getEngineHost(); - const [collectionName] = this.parseSchematicInfo(options); - const collection = getCollection(collectionName); - const schematicNames: string[] = engineHost.listSchematics(collection); - this.logger.info('Available schematics:'); - schematicNames.forEach(schematicName => { - this.logger.info(` ${schematicName}`); - }); - - this.logger.warn(`\nTo see help for a schematic run:`); - this.logger.info(terminal.cyan(` ng generate --help`)); - } - } -} diff --git a/packages/angular/cli/commands/getset.ts b/packages/angular/cli/commands/getset.ts deleted file mode 100644 index 6da1b2f2b5e7..000000000000 --- a/packages/angular/cli/commands/getset.ts +++ /dev/null @@ -1,26 +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 { Command, Option } from '../models/command'; - -export interface Options { - keyword: string; - search?: boolean; -} - -export class GetSetCommand extends Command { - public readonly name = 'getset'; - public readonly description = 'Deprecated in favor of config command.'; - public readonly arguments: string[] = []; - public readonly options: Option[] = []; - public readonly hidden = true; - - public async run(_options: Options) { - this.logger.warn('get/set have been deprecated in favor of the config command.'); - } -} diff --git a/packages/angular/cli/commands/help.ts b/packages/angular/cli/commands/help.ts deleted file mode 100644 index 4c59dd3a98da..000000000000 --- a/packages/angular/cli/commands/help.ts +++ /dev/null @@ -1,44 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { terminal } from '@angular-devkit/core'; -import { Command, Option } from '../models/command'; - - -export class HelpCommand extends Command { - public readonly name = 'help'; - public readonly description = 'Help.'; - public readonly arguments: string[] = []; - public readonly options: Option[] = []; - - run(options: any) { - const commands = Object.keys(options.commandMap) - .map(key => { - const Cmd = options.commandMap[key]; - const command: Command = new Cmd(null, null); - - return command; - }) - .filter(cmd => !cmd.hidden && !cmd.unknown) - .map(cmd => ({ - name: cmd.name, - description: cmd.description, - })); - this.logger.info(`Available Commands:`); - commands.forEach(cmd => { - this.logger.info(` ${terminal.cyan(cmd.name)} ${cmd.description}`); - }); - - this.logger.info(`\nFor more detailed help run "ng [command name] --help"`); - } - - printHelp(options: any) { - return this.run(options); - } -} diff --git a/packages/angular/cli/commands/lint.ts b/packages/angular/cli/commands/lint.ts deleted file mode 100644 index 492931da53d5..000000000000 --- a/packages/angular/cli/commands/lint.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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { CommandScope, Option } from '../models/command'; - - -export class LintCommand extends ArchitectCommand { - public readonly name = 'lint'; - public readonly target = 'lint'; - public readonly description = 'Lints code in existing project.'; - public static aliases = ['l']; - public static scope = CommandScope.inProject; - public readonly multiTarget = true; - public readonly options: Option[] = [ - this.configurationOption, - ]; - - public async run(options: ArchitectCommandOptions) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/new.ts b/packages/angular/cli/commands/new.ts deleted file mode 100644 index 94697b63deaa..000000000000 --- a/packages/angular/cli/commands/new.ts +++ /dev/null @@ -1,106 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { CommandScope, Option } from '../models/command'; -import { SchematicCommand } from '../models/schematic-command'; -import { getDefaultSchematicCollection } from '../utilities/config'; - - -export class NewCommand extends SchematicCommand { - public readonly name = 'new'; - public readonly description = - 'Creates a new directory and a new Angular app.'; - public static aliases = ['n']; - public static scope = CommandScope.outsideProject; - public readonly allowMissingWorkspace = true; - public arguments: string[] = []; - public options: Option[] = [ - ...this.coreOptions, - { - name: 'verbose', - type: Boolean, - default: false, - aliases: ['v'], - description: 'Adds more details to output logging.', - }, - { - name: 'collection', - type: String, - aliases: ['c'], - description: 'Schematics collection to use.', - }, - ]; - private schematicName = 'ng-new'; - - private initialized = false; - public async initialize(options: any) { - if (this.initialized) { - return; - } - - await super.initialize(options); - - this.initialized = true; - - const collectionName = this.parseCollectionName(options); - - const schematicOptions = await this.getOptions({ - schematicName: this.schematicName, - collectionName, - }); - - this.options = this.options.concat(schematicOptions.options); - const args = schematicOptions.arguments.map(arg => arg.name); - this.arguments = this.arguments.concat(args); - } - - public async run(options: any) { - if (options.dryRun) { - options.skipGit = true; - } - - let collectionName: string; - if (options.collection) { - collectionName = options.collection; - } else { - collectionName = this.parseCollectionName(options); - } - - const packageJson = require('../package.json'); - options.version = packageJson.version; - - // Ensure skipGit has a boolean value. - options.skipGit = options.skipGit === undefined ? false : options.skipGit; - - options = this.removeLocalOptions(options); - - return this.runSchematic({ - collectionName: collectionName, - schematicName: this.schematicName, - schematicOptions: options, - debug: options.debug, - dryRun: options.dryRun, - force: options.force, - }); - } - - private parseCollectionName(options: any): string { - const collectionName = options.collection || options.c || getDefaultSchematicCollection(); - - return collectionName; - } - - private removeLocalOptions(options: any): any { - const opts = Object.assign({}, options); - delete opts.verbose; - delete opts.collection; - - return opts; - } -} diff --git a/packages/angular/cli/commands/run.ts b/packages/angular/cli/commands/run.ts deleted file mode 100644 index 4f78621c6171..000000000000 --- a/packages/angular/cli/commands/run.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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { CommandScope, Option } from '../models/command'; - - -export class RunCommand extends ArchitectCommand { - public readonly name = 'run'; - public readonly description = 'Runs Architect targets.'; - public static scope = CommandScope.inProject; - public static aliases = []; - public readonly arguments: string[] = ['target']; - public readonly options: Option[] = [ - this.configurationOption, - ]; - - public async run(options: ArchitectCommandOptions) { - if (options.target) { - return this.runArchitectTarget(options); - } else { - throw new Error('Invalid architect target.'); - } - } -} diff --git a/packages/angular/cli/commands/serve.ts b/packages/angular/cli/commands/serve.ts deleted file mode 100644 index 88b4f6b17a68..000000000000 --- a/packages/angular/cli/commands/serve.ts +++ /dev/null @@ -1,36 +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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { CommandScope, Option } from '../models/command'; -import { Version } from '../upgrade/version'; - - -export class ServeCommand extends ArchitectCommand { - public readonly name = 'serve'; - public readonly target = 'serve'; - public readonly description = 'Builds and serves your app, rebuilding on file changes.'; - public static aliases = ['s']; - public static scope = CommandScope.inProject; - public readonly options: Option[] = [ - this.prodOption, - this.configurationOption, - ]; - - public validate(_options: ArchitectCommandOptions) { - // Check Angular and TypeScript versions. - Version.assertCompatibleAngularVersion(this.project.root); - Version.assertTypescriptVersion(this.project.root); - - return true; - } - - public async run(options: ArchitectCommandOptions) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/test.ts b/packages/angular/cli/commands/test.ts deleted file mode 100644 index 3cdd7e1ff009..000000000000 --- a/packages/angular/cli/commands/test.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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { CommandScope, Option } from '../models/command'; - - -export class TestCommand extends ArchitectCommand { - public readonly name = 'test'; - public readonly target = 'test'; - public readonly description = 'Run unit tests in existing project.'; - public static aliases = ['t']; - public static scope = CommandScope.inProject; - public readonly multiTarget = true; - public readonly options: Option[] = [ - this.prodOption, - this.configurationOption, - ]; - - public async run(options: ArchitectCommandOptions) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/update.ts b/packages/angular/cli/commands/update.ts deleted file mode 100644 index ebf26982839d..000000000000 --- a/packages/angular/cli/commands/update.ts +++ /dev/null @@ -1,85 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { normalize } from '@angular-devkit/core'; -import { CommandScope, Option } from '../models/command'; -import { CoreSchematicOptions, SchematicCommand } from '../models/schematic-command'; -import { findUp } from '../utilities/find-up'; - -export interface UpdateOptions extends CoreSchematicOptions { - next: boolean; - schematic?: boolean; -} - - -export class UpdateCommand extends SchematicCommand { - public readonly name = 'update'; - public readonly description = 'Updates your application and its dependencies.'; - public static aliases: string[] = []; - public static scope = CommandScope.everywhere; - public arguments: string[] = [ 'packages' ]; - public options: Option[] = [ - // Remove the --force flag. - ...this.coreOptions.filter(option => option.name !== 'force'), - ]; - public readonly allowMissingWorkspace = true; - - private collectionName = '@schematics/update'; - private schematicName = 'update'; - - private initialized = false; - public async initialize(options: any) { - if (this.initialized) { - return; - } - await super.initialize(options); - this.initialized = true; - - const schematicOptions = await this.getOptions({ - schematicName: this.schematicName, - collectionName: this.collectionName, - }); - this.options = this.options.concat(schematicOptions.options); - this.arguments = this.arguments.concat(schematicOptions.arguments.map(a => a.name)); - } - - async validate(options: any) { - if (options._[0] == '@angular/cli' - && options.migrateOnly === undefined - && options.from === undefined) { - // Check for a 1.7 angular-cli.json file. - const oldConfigFileNames = [ - normalize('.angular-cli.json'), - normalize('angular-cli.json'), - ]; - const oldConfigFilePath = - findUp(oldConfigFileNames, process.cwd()) - || findUp(oldConfigFileNames, __dirname); - - if (oldConfigFilePath) { - options.migrateOnly = true; - options.from = '1.0.0'; - } - } - - return super.validate(options); - } - - - public async run(options: UpdateOptions) { - return this.runSchematic({ - collectionName: this.collectionName, - schematicName: this.schematicName, - schematicOptions: options, - dryRun: options.dryRun, - force: false, - showNothingDone: false, - }); - } -} diff --git a/packages/angular/cli/commands/version.ts b/packages/angular/cli/commands/version.ts deleted file mode 100644 index d46d9324709a..000000000000 --- a/packages/angular/cli/commands/version.ts +++ /dev/null @@ -1,183 +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 { terminal } from '@angular-devkit/core'; -import * as child_process from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import { Command, Option } from '../models/command'; -import { findUp } from '../utilities/find-up'; - - -export class VersionCommand extends Command { - public readonly name = 'version'; - public readonly description = 'Outputs Angular CLI version.'; - public static aliases = ['v']; - public readonly arguments: string[] = []; - public readonly options: Option[] = []; - - public run() { - const pkg = require(path.resolve(__dirname, '..', 'package.json')); - let projPkg; - try { - projPkg = require(path.resolve(this.project.root, 'package.json')); - } catch (exception) { - projPkg = undefined; - } - - const patterns = [ - /^@angular\/.*/, - /^@angular-devkit\/.*/, - /^@ngtools\/.*/, - /^@schematics\/.*/, - /^rxjs$/, - /^typescript$/, - /^ng-packagr$/, - /^webpack$/, - ]; - - const maybeNodeModules = findUp('node_modules', __dirname); - const packageRoot = projPkg - ? path.resolve(this.project.root, 'node_modules') - : maybeNodeModules; - - const packageNames = [ - ...Object.keys(pkg && pkg['dependencies'] || {}), - ...Object.keys(pkg && pkg['devDependencies'] || {}), - ...Object.keys(projPkg && projPkg['dependencies'] || {}), - ...Object.keys(projPkg && projPkg['devDependencies'] || {}), - ]; - - if (packageRoot != null) { - // Add all node_modules and node_modules/@*/* - const nodePackageNames = fs.readdirSync(packageRoot) - .reduce((acc, name) => { - if (name.startsWith('@')) { - return acc.concat( - fs.readdirSync(path.resolve(packageRoot, name)) - .map(subName => name + '/' + subName), - ); - } else { - return acc.concat(name); - } - }, []); - - packageNames.push(...nodePackageNames); - } - - const versions = packageNames - .filter(x => patterns.some(p => p.test(x))) - .reduce((acc, name) => { - if (name in acc) { - return acc; - } - - acc[name] = this.getVersion(name, packageRoot, maybeNodeModules); - - return acc; - }, {} as { [module: string]: string }); - - let ngCliVersion = pkg.version; - if (!__dirname.match(/node_modules/)) { - let gitBranch = '??'; - try { - const gitRefName = '' + child_process.execSync('git symbolic-ref HEAD', {cwd: __dirname}); - gitBranch = path.basename(gitRefName.replace('\n', '')); - } catch { - } - - ngCliVersion = `local (v${pkg.version}, branch: ${gitBranch})`; - } - let angularCoreVersion = ''; - const angularSameAsCore: string[] = []; - - if (projPkg) { - // Filter all angular versions that are the same as core. - angularCoreVersion = versions['@angular/core']; - if (angularCoreVersion) { - for (const angularPackage of Object.keys(versions)) { - if (versions[angularPackage] == angularCoreVersion - && angularPackage.startsWith('@angular/')) { - angularSameAsCore.push(angularPackage.replace(/^@angular\//, '')); - delete versions[angularPackage]; - } - } - - // Make sure we list them in alphabetical order. - angularSameAsCore.sort(); - } - } - - const namePad = ' '.repeat( - Object.keys(versions).sort((a, b) => b.length - a.length)[0].length + 3, - ); - const asciiArt = ` - _ _ ____ _ ___ - / \\ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _| - / △ \\ | '_ \\ / _\` | | | | |/ _\` | '__| | | | | | | - / ___ \\| | | | (_| | |_| | | (_| | | | |___| |___ | | - /_/ \\_\\_| |_|\\__, |\\__,_|_|\\__,_|_| \\____|_____|___| - |___/ - `.split('\n').map(x => terminal.red(x)).join('\n'); - - this.logger.info(asciiArt); - this.logger.info(` - Angular CLI: ${ngCliVersion} - Node: ${process.versions.node} - OS: ${process.platform} ${process.arch} - Angular: ${angularCoreVersion} - ... ${angularSameAsCore.reduce((acc, name) => { - // Perform a simple word wrap around 60. - if (acc.length == 0) { - return [name]; - } - const line = (acc[acc.length - 1] + ', ' + name); - if (line.length > 60) { - acc.push(name); - } else { - acc[acc.length - 1] = line; - } - - return acc; - }, []).join('\n... ')} - - Package${namePad.slice(7)}Version - -------${namePad.replace(/ /g, '-')}------------------ - ${Object.keys(versions) - .map(module => `${module}${namePad.slice(module.length)}${versions[module]}`) - .sort() - .join('\n')} - `.replace(/^ {6}/gm, '')); - } - - private getVersion( - moduleName: string, - projectNodeModules: string | null, - cliNodeModules: string | null, - ): string { - try { - if (projectNodeModules) { - const modulePkg = require(path.resolve(projectNodeModules, moduleName, 'package.json')); - - return modulePkg.version; - } - } catch (_) { - } - - try { - if (cliNodeModules) { - const modulePkg = require(path.resolve(cliNodeModules, moduleName, 'package.json')); - - return modulePkg.version + ' (cli-only)'; - } - } catch { - } - - return ''; - } -} diff --git a/packages/angular/cli/commands/xi18n.ts b/packages/angular/cli/commands/xi18n.ts deleted file mode 100644 index eb0d39a470f1..000000000000 --- a/packages/angular/cli/commands/xi18n.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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { CommandScope, Option } from '../models/command'; - - -export class Xi18nCommand extends ArchitectCommand { - public readonly name = 'xi18n'; - public readonly target = 'extract-i18n'; - public readonly description = 'Extracts i18n messages from source code.'; - public static scope = CommandScope.inProject; - public static aliases = []; - public readonly multiTarget: true; - public readonly options: Option[] = [ - this.configurationOption, - ]; - - public async run(options: ArchitectCommandOptions) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/custom-typings.d.ts b/packages/angular/cli/custom-typings.d.ts deleted file mode 100644 index f4b19069aef2..000000000000 --- a/packages/angular/cli/custom-typings.d.ts +++ /dev/null @@ -1,14 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -declare module 'yargs-parser' { - const parseOptions: any; - const yargsParser: (args: string | string[], options?: typeof parseOptions) => T; - export = yargsParser; -} diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index 92578a3e0cd7..a2566853dfc7 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -1,147 +1,117 @@ /** * @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 { logging, terminal } from '@angular-devkit/core'; -import { filter } from 'rxjs/operators'; -import { AddCommand } from '../../commands/add'; -import { BuildCommand } from '../../commands/build'; -import { ConfigCommand } from '../../commands/config'; -import { DocCommand } from '../../commands/doc'; -import { E2eCommand } from '../../commands/e2e'; -import { AwesomeCommand } from '../../commands/easter-egg'; -import { EjectCommand } from '../../commands/eject'; -import { GenerateCommand } from '../../commands/generate'; -import { GetSetCommand } from '../../commands/getset'; -import { HelpCommand } from '../../commands/help'; -import { LintCommand } from '../../commands/lint'; -import { NewCommand } from '../../commands/new'; -import { RunCommand } from '../../commands/run'; -import { ServeCommand } from '../../commands/serve'; -import { TestCommand } from '../../commands/test'; -import { UpdateCommand } from '../../commands/update'; -import { VersionCommand } from '../../commands/version'; -import { Xi18nCommand } from '../../commands/xi18n'; -import { CommandMap, runCommand } from '../../models/command-runner'; -import { getProjectDetails } from '../../utilities/project'; - - -async function loadCommands(): Promise { - return { - // Schematics commands. - 'add': AddCommand, - 'new': NewCommand, - 'generate': GenerateCommand, - 'update': UpdateCommand, - - // Architect commands. - 'build': BuildCommand, - 'serve': ServeCommand, - 'test': TestCommand, - 'e2e': E2eCommand, - 'lint': LintCommand, - 'xi18n': Xi18nCommand, - 'run': RunCommand, - - // Disabled commands. - 'eject': EjectCommand, - - // Easter eggs. - 'make-this-awesome': AwesomeCommand, - - // Other. - 'config': ConfigCommand, - 'help': HelpCommand, - 'version': VersionCommand, - 'doc': DocCommand, +import { logging } from '@angular-devkit/core'; +import { format, stripVTControlCharacters } from 'node:util'; +import { CommandModuleError } from '../../src/command-builder/command-module'; +import { runCommand } from '../../src/command-builder/command-runner'; +import { colors, supportColor } from '../../src/utilities/color'; +import { ngDebug } from '../../src/utilities/environment-options'; +import { writeErrorToLogFile } from '../../src/utilities/log-file'; + +export { VERSION } from '../../src/utilities/version'; + +const MIN_NODEJS_VERSION = [20, 11] as const; + +/* eslint-disable no-console */ +export default async function (options: { cliArgs: string[] }) { + // This node version check ensures that the requirements of the project instance of the CLI are met + const [major, minor] = process.versions.node.split('.').map((part) => Number(part)); + if ( + major < MIN_NODEJS_VERSION[0] || + (major === MIN_NODEJS_VERSION[0] && minor < MIN_NODEJS_VERSION[1]) + ) { + process.stderr.write( + `Node.js version ${process.version} detected.\n` + + `The Angular CLI requires a minimum of v${MIN_NODEJS_VERSION[0]}.${MIN_NODEJS_VERSION[1]}.\n\n` + + 'Please update your Node.js version or visit https://nodejs.org/ for additional instructions.\n', + ); + + return 3; + } - // deprecated - 'get': GetSetCommand, - 'set': GetSetCommand, + const colorLevels: Record string> = { + info: (s) => s, + debug: (s) => s, + warn: (s) => colors.bold(colors.yellow(s)), + error: (s) => colors.bold(colors.red(s)), + fatal: (s) => colors.bold(colors.red(s)), }; -} - -export default async function(options: { testing?: boolean, cliArgs: string[] }) { - const commands = await loadCommands(); + const logger = new logging.IndentLogger('cli-main-logger'); + const logInfo = console.log; + const logError = console.error; + const useColor = supportColor(); + + const loggerFinished = logger.forEach((entry) => { + if (!ngDebug && entry.level === 'debug') { + return; + } - const logger = new logging.IndentLogger('cling'); - let loggingSubscription; - if (!options.testing) { - loggingSubscription = initializeLogging(logger); - } + const color = useColor ? colorLevels[entry.level] : stripVTControlCharacters; + const message = color(entry.message); + + switch (entry.level) { + case 'warn': + case 'fatal': + case 'error': + logError(message); + break; + default: + logInfo(message); + break; + } + }); - let projectDetails = getProjectDetails(); - if (projectDetails === null) { - projectDetails = { root: process.cwd() }; - } - const context = { - project: projectDetails, + // Redirect console to logger + console.info = console.log = function (...args) { + logger.info(format(...args)); + }; + console.warn = function (...args) { + logger.warn(format(...args)); + }; + console.error = function (...args) { + logger.error(format(...args)); }; try { - const maybeExitCode = await runCommand(commands, options.cliArgs, logger, context); - if (typeof maybeExitCode === 'number') { - console.assert(Number.isInteger(maybeExitCode)); - - return maybeExitCode; - } - - return 0; + return await runCommand(options.cliArgs, logger); } catch (err) { - if (err instanceof Error) { - logger.fatal(err.message); - if (err.stack) { - logger.fatal(err.stack); + if (err instanceof CommandModuleError) { + logger.fatal(`Error: ${err.message}`); + } else if (err instanceof Error) { + try { + const logPath = writeErrorToLogFile(err); + logger.fatal( + `An unhandled exception occurred: ${err.message}\n` + + `See "${logPath}" for further details.`, + ); + } catch (e) { + logger.fatal( + `An unhandled exception occurred: ${err.message}\n` + + `Fatal error writing debug log file: ${e}`, + ); + if (err.stack) { + logger.fatal(err.stack); + } } + + return 127; } else if (typeof err === 'string') { logger.fatal(err); } else if (typeof err === 'number') { // Log nothing. } else { - logger.fatal('An unexpected error occurred: ' + JSON.stringify(err)); - } - - if (options.testing) { - debugger; - throw err; - } - - if (loggingSubscription) { - loggingSubscription.unsubscribe(); + logger.fatal(`An unexpected error occurred: ${err}`); } return 1; + } finally { + logger.complete(); + await loggerFinished; } } - -// Initialize logging. -function initializeLogging(logger: logging.Logger) { - return logger - .pipe(filter(entry => (entry.level != 'debug'))) - .subscribe(entry => { - let color = (x: string) => terminal.dim(terminal.white(x)); - let output = process.stdout; - switch (entry.level) { - case 'info': - color = terminal.white; - break; - case 'warn': - color = terminal.yellow; - break; - case 'error': - color = terminal.red; - output = process.stderr; - break; - case 'fatal': - color = (x) => terminal.bold(terminal.red(x)); - output = process.stderr; - break; - } - - output.write(color(entry.message) + '\n'); - }); -} diff --git a/packages/angular/cli/lib/config/.gitignore b/packages/angular/cli/lib/config/.gitignore deleted file mode 100644 index 879ebeae0a5f..000000000000 --- a/packages/angular/cli/lib/config/.gitignore +++ /dev/null @@ -1 +0,0 @@ -schema.d.ts \ No newline at end of file diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json deleted file mode 100644 index 18628f59599b..000000000000 --- a/packages/angular/cli/lib/config/schema.json +++ /dev/null @@ -1,1618 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "id": "https://angular.io/schemas/cli-1/schema", - "title": "Angular CLI Configuration", - "type": "object", - "properties": { - "$schema": { - "type": "string" - }, - "version": { - "$ref": "#/definitions/fileVersion" - }, - "cli": { - "$ref": "#/definitions/cliOptions" - }, - "schematics": { - "$ref": "#/definitions/schematicOptions" - }, - "newProjectRoot": { - "type": "string", - "description": "Path where new projects will be created." - }, - "defaultProject": { - "type": "string", - "description": "Default project name used in commands." - }, - "projects": { - "type": "object", - "patternProperties": { - "^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$": { - "$ref": "#/definitions/project" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false, - "required": [ - "version" - ], - "definitions": { - "cliOptions": { - "type": "object", - "properties": { - "defaultCollection": { - "description": "The default schematics collection to use.", - "type": "string" - }, - "packageManager": { - "description": "Specify which package manager tool to use.", - "type": "string", - "enum": [ "npm", "cnpm", "yarn" ] - }, - "warnings": { - "description": "Control CLI specific console warnings", - "type": "object", - "properties": { - "versionMismatch": { - "description": "Show a warning when the global version is newer than the local one.", - "type": "boolean" - }, - "typescriptMismatch": { - "description": "Show a warning when the TypeScript version is incompatible.", - "type": "boolean" - } - } - } - }, - "additionalProperties": false - }, - "schematicOptions": { - "type": "object", - "properties": { - "@schematics/angular:component": { - "type": "object", - "properties": { - "changeDetection": { - "description": "Specifies the change detection strategy.", - "enum": ["Default", "OnPush"], - "type": "string", - "default": "Default", - "alias": "c" - }, - "entryComponent": { - "type": "boolean", - "default": false, - "description": "Specifies if the component is an entry component of declaring module." - }, - "export": { - "type": "boolean", - "default": false, - "description": "Specifies if declaring module exports the component." - }, - "flat": { - "type": "boolean", - "description": "Flag to indicate if a dir is created.", - "default": false - }, - "inlineStyle": { - "description": "Specifies if the style will be in the ts file.", - "type": "boolean", - "default": false, - "alias": "s" - }, - "inlineTemplate": { - "description": "Specifies if the template will be in the ts file.", - "type": "boolean", - "default": false, - "alias": "t" - }, - "module": { - "type": "string", - "description": "Allows specification of the declaring module.", - "alias": "m" - }, - "prefix": { - "type": "string", - "format": "html-selector", - "description": "The prefix to apply to generated selectors.", - "alias": "p" - }, - "selector": { - "type": "string", - "format": "html-selector", - "description": "The selector to use for the component." - }, - "skipImport": { - "type": "boolean", - "description": "Flag to skip the module import.", - "default": false - }, - "spec": { - "type": "boolean", - "description": "Specifies if a spec file is generated.", - "default": true - }, - "styleext": { - "description": "The file extension to be used for style files.", - "type": "string", - "default": "css" - }, - "viewEncapsulation": { - "description": "Specifies the view encapsulation strategy.", - "enum": ["Emulated", "Native", "None"], - "type": "string", - "alias": "v" - } - } - }, - "@schematics/angular:directive": { - "type": "object", - "properties": { - "export": { - "type": "boolean", - "default": false, - "description": "Specifies if declaring module exports the directive." - }, - "flat": { - "type": "boolean", - "description": "Flag to indicate if a dir is created.", - "default": true - }, - "module": { - "type": "string", - "description": "Allows specification of the declaring module.", - "alias": "m" - }, - "prefix": { - "type": "string", - "format": "html-selector", - "description": "The prefix to apply to generated selectors.", - "default": "app", - "alias": "p" - }, - "selector": { - "type": "string", - "format": "html-selector", - "description": "The selector to use for the directive." - }, - "skipImport": { - "type": "boolean", - "description": "Flag to skip the module import.", - "default": false - }, - "spec": { - "type": "boolean", - "description": "Specifies if a spec file is generated.", - "default": true - } - } - }, - "@schematics/angular:module": { - "type": "object", - "properties": { - "routing": { - "type": "boolean", - "description": "Generates a routing module.", - "default": false - }, - "routingScope": { - "enum": ["Child", "Root"], - "type": "string", - "description": "The scope for the generated routing.", - "default": "Child" - }, - "spec": { - "type": "boolean", - "description": "Specifies if a spec file is generated.", - "default": true - }, - "flat": { - "type": "boolean", - "description": "Flag to indicate if a dir is created.", - "default": false - }, - "commonModule": { - "type": "boolean", - "description": "Flag to control whether the CommonModule is imported.", - "default": true, - "visible": false - }, - "module": { - "type": "string", - "description": "Allows specification of the declaring module.", - "alias": "m" - } - } - }, - "@schematics/angular:service": { - "type": "object", - "properties": { - "flat": { - "type": "boolean", - "default": true, - "description": "Flag to indicate if a dir is created." - }, - "spec": { - "type": "boolean", - "default": true, - "description": "Specifies if a spec file is generated." - } - } - }, - "@schematics/angular:pipe": { - "type": "object", - "properties": { - "flat": { - "type": "boolean", - "default": true, - "description": "Flag to indicate if a dir is created." - }, - "spec": { - "type": "boolean", - "default": true, - "description": "Specifies if a spec file is generated." - }, - "skipImport": { - "type": "boolean", - "default": false, - "description": "Allows for skipping the module import." - }, - "module": { - "type": "string", - "default": "", - "description": "Allows specification of the declaring module.", - "alias": "m" - }, - "export": { - "type": "boolean", - "default": false, - "description": "Specifies if declaring module exports the pipe." - } - } - }, - "@schematics/angular:class": { - "type": "object", - "properties": { - "spec": { - "type": "boolean", - "default": true, - "description": "Specifies if a spec file is generated." - } - } - } - }, - "additionalProperties": { - "type": "object" - } - }, - "fileVersion": { - "type": "integer", - "description": "File format version", - "minimum": 1 - }, - "project": { - "type": "object", - "properties": { - "cli": { - "$ref": "#/definitions/cliOptions" - }, - "schematics": { - "$ref": "#/definitions/schematicOptions" - }, - "prefix": { - "type": "string", - "format": "html-selector", - "description": "The prefix to apply to generated selectors." - }, - "root": { - "type": "string", - "description": "Root of the project files." - }, - "sourceRoot": { - "type": "string", - "description": "The root of the source files, assets and index.html file structure." - }, - "projectType": { - "type": "string", - "description": "Project type.", - "enum": [ - "application", - "library" - ] - }, - "architect": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/project/definitions/target" - } - } - }, - "required": [ - "root", - "projectType" - ], - "additionalProperties": false, - "patternProperties": { - "^[a-z]{1,3}-.*": {} - }, - "definitions": { - "target": { - "oneOf": [ - { - "$comment": "Extendable target with custom builder", - "type": "object", - "properties": { - "builder": { - "type": "string", - "description": "The builder used for this package.", - "not": { - "enum": [ - "@angular-devkit/build-angular:app-shell", - "@angular-devkit/build-angular:browser", - "@angular-devkit/build-angular:dev-server", - "@angular-devkit/build-angular:extract-i18n", - "@angular-devkit/build-angular:karma", - "@angular-devkit/build-angular:protractor", - "@angular-devkit/build-angular:server", - "@angular-devkit/build-angular:tslint" - ] - } - }, - "options": { - "type": "object" - }, - "configurations": { - "type": "object", - "description": "A map of alternative target options.", - "additionalProperties": { - "type": "object" - } - } - }, - "required": [ - "builder" - ] - }, - { - "type": "object", - "properties": { - "builder": { "const": "@angular-devkit/build-angular:app-shell" }, - "options": { "$ref": "#/definitions/targetOptions/definitions/appShell" }, - "configurations": { - "type": "object", - "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/appShell" } - } - } - }, - { - "type": "object", - "properties": { - "builder": { "const": "@angular-devkit/build-angular:browser" }, - "options": { "$ref": "#/definitions/targetOptions/definitions/browser" }, - "configurations": { - "type": "object", - "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/browser" } - } - } - }, - { - "type": "object", - "properties": { - "builder": { "const": "@angular-devkit/build-angular:dev-server" }, - "options": { "$ref": "#/definitions/targetOptions/definitions/devServer" }, - "configurations": { - "type": "object", - "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/devServer" } - } - } - }, - { - "type": "object", - "properties": { - "builder": { "const": "@angular-devkit/build-angular:extract-i18n" }, - "options": { "$ref": "#/definitions/targetOptions/definitions/extracti18n" }, - "configurations": { - "type": "object", - "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/extracti18n" } - } - } - }, - { - "type": "object", - "properties": { - "builder": { "const": "@angular-devkit/build-angular:karma" }, - "options": { "$ref": "#/definitions/targetOptions/definitions/karma" }, - "configurations": { - "type": "object", - "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/karma" } - } - } - }, - { - "type": "object", - "properties": { - "builder": { "const": "@angular-devkit/build-angular:protractor" }, - "options": { "$ref": "#/definitions/targetOptions/definitions/protractor" }, - "configurations": { - "type": "object", - "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/protractor" } - } - } - }, - { - "type": "object", - "properties": { - "builder": { "const": "@angular-devkit/build-angular:server" }, - "options": { "$ref": "#/definitions/targetOptions/definitions/server" }, - "configurations": { - "type": "object", - "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/server" } - } - } - }, - { - "type": "object", - "properties": { - "builder": { "const": "@angular-devkit/build-angular:tslint" }, - "options": { "$ref": "#/definitions/targetOptions/definitions/tslint" }, - "configurations": { - "type": "object", - "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/tslint" } - } - } - } - ] - } - } - }, - "global": { - "type": "object", - "properties": { - "$schema": { - "type": "string", - "format": "uri" - }, - "version": { - "$ref": "#/definitions/fileVersion" - }, - "cli": { - "$ref": "#/definitions/cliOptions" - }, - "schematics": { - "$ref": "#/definitions/schematicOptions" - } - }, - "required": [ - "version" - ] - }, - "targetOptions": { - "type": "null", - "definitions": { - "appShell": { - "description": "App Shell target options for Build Facade.", - "type": "object", - "properties": { - "browserTarget": { - "type": "string", - "description": "Target to build." - }, - "serverTarget": { - "type": "string", - "description": "Server target to use for rendering the app shell." - }, - "appModuleBundle": { - "type": "string", - "description": "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 serverTarget and find a bundle named 'main' in it (whether or not there's a hash tag)." - }, - "route": { - "type": "string", - "description": "The route to render.", - "default": "/" - }, - "inputIndexPath": { - "type": "string", - "description": "The input path for the index.html file. By default uses the output index.html of the browser target." - }, - "outputIndexPath": { - "type": "string", - "description": "The output path of the index.html file. By default will overwrite the input file." - } - }, - "additionalProperties": false - }, - "browser": { - "title": "Webpack browser schema for Build Facade.", - "description": "Browser target options", - "properties": { - "assets": { - "type": "array", - "description": "List of static application assets.", - "default": [], - "items": { - "$ref": "#/definitions/targetOptions/definitions/browser/definitions/assetPattern" - } - }, - "main": { - "type": "string", - "description": "The name of the main entry-point file." - }, - "polyfills": { - "type": "string", - "description": "The name of the polyfills file." - }, - "tsConfig": { - "type": "string", - "description": "The name of the TypeScript configuration file." - }, - "scripts": { - "description": "Global scripts to be included in the build.", - "type": "array", - "default": [], - "items": { - "$ref": "#/definitions/targetOptions/definitions/browser/definitions/extraEntryPoint" - } - }, - "styles": { - "description": "Global styles to be included in the build.", - "type": "array", - "default": [], - "items": { - "$ref": "#/definitions/targetOptions/definitions/browser/definitions/extraEntryPoint" - } - }, - "stylePreprocessorOptions": { - "description": "Options to pass to style preprocessors.", - "type": "object", - "properties": { - "includePaths": { - "description": "Paths to include. Paths will be resolved to project root.", - "type": "array", - "items": { - "type": "string" - }, - "default": [] - } - }, - "additionalProperties": false - }, - "optimization": { - "type": "boolean", - "description": "Defines the optimization level of the build.", - "default": false - }, - "fileReplacements": { - "description": "Replace files with other files in the build.", - "type": "array", - "items": { - "$ref": "#/definitions/targetOptions/definitions/browser/definitions/fileReplacement" - }, - "default": [] - }, - "outputPath": { - "type": "string", - "description": "Path where output will be placed." - }, - "aot": { - "type": "boolean", - "description": "Build using Ahead of Time compilation.", - "default": false - }, - "sourceMap": { - "type": "boolean", - "description": "Output sourcemaps.", - "default": true - }, - "evalSourceMap": { - "type": "boolean", - "description": "Output in-file eval sourcemaps.", - "default": false - }, - "vendorChunk": { - "type": "boolean", - "description": "Use a separate bundle containing only vendor libraries.", - "default": true - }, - "commonChunk": { - "type": "boolean", - "description": "Use a separate bundle containing code used across multiple bundles.", - "default": true - }, - "baseHref": { - "type": "string", - "description": "Base url for the application being built." - }, - "deployUrl": { - "type": "string", - "description": "URL where files will be deployed." - }, - "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 - }, - "i18nFile": { - "type": "string", - "description": "Localization file to use for i18n." - }, - "i18nFormat": { - "type": "string", - "description": "Format of the localization file specified with --i18n-file." - }, - "i18nLocale": { - "type": "string", - "description": "Locale to use for i18n." - }, - "i18nMissingTranslation": { - "type": "string", - "description": "How to handle missing translations for i18n." - }, - "extractCss": { - "type": "boolean", - "description": "Extract css from global styles onto css files instead of js ones.", - "default": false - }, - "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.", - "default": false - }, - "extractLicenses": { - "type": "boolean", - "description": "Extract all licenses in a separate file, in the case of production builds only.", - "default": true - }, - "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 - }, - "namedChunks": { - "type": "boolean", - "description": "Use file name for lazy loaded chunks.", - "default": true - }, - "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 - }, - "skipAppShell": { - "type": "boolean", - "description": "Flag to prevent building an app shell.", - "default": false - }, - "index": { - "type": "string", - "description": "The name of the index HTML file." - }, - "statsJson": { - "type": "boolean", - "description": "Generates a 'stats.json' file which can be analyzed using tools such as: #webpack-bundle-analyzer' or https://webpack.github.io/analyse .", - "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 with be discovered automatically.", - "type": "array", - "items": { - "type": "string" - }, - "default": [] - }, - "budgets": { - "description": "Budget thresholds to ensure parts of your application stay within boundaries which you set.", - "type": "array", - "items": { - "$ref": "#/definitions/targetOptions/definitions/browser/definitions/budget" - }, - "default": [] - } - }, - "additionalProperties": false, - "definitions": { - "assetPattern": { - "oneOf": [ - { - "type": "object", - "properties": { - "glob": { - "type": "string", - "description": "The pattern to match." - }, - "input": { - "type": "string", - "description": "The input path dir in which to apply 'glob'. Defaults to the project root." - }, - "output": { - "type": "string", - "description": "Absolute path within the output." - } - }, - "additionalProperties": false, - "required": [ - "glob", - "input", - "output" - ] - }, - { - "type": "string", - "description": "The file to include." - } - ] - }, - "fileReplacement": { - "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" - ] - } - ] - }, - "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 - } - }, - "additionalProperties": false, - "required": [ - "input" - ] - }, - { - "type": "string", - "description": "The file to include." - } - ] - }, - "budget": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "The type of budget.", - "enum": [ - "all", - "allScript", - "any", - "anyScript", - "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" - ] - } - } - }, - "devServer": { - "description": "Dev Server target options for Build Facade.", - "type": "object", - "properties": { - "browserTarget": { - "type": "string", - "description": "Target to serve." - }, - "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" - }, - "liveReload": { - "type": "boolean", - "description": "Whether to reload the page on change, using live-reload.", - "default": true - }, - "publicHost": { - "type": "string", - "description": "Specify the URL that the browser client will use." - }, - "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": { - "type": "boolean", - "description": "Defines the optimization level of the build." - }, - "aot": { - "type": "boolean", - "description": "Build using Ahead of Time compilation." - }, - "sourceMap": { - "type": "boolean", - "description": "Output sourcemaps." - }, - "evalSourceMap": { - "type": "boolean", - "description": "Output in-file eval sourcemaps." - }, - "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." - }, - "verbose": { - "type": "boolean", - "description": "Adds more details to output logging." - }, - "progress": { - "type": "boolean", - "description": "Log progress to the console while building." - } - }, - "additionalProperties": false - }, - "extracti18n": { - "description": "Extract i18n target options for Build Facade.", - "type": "object", - "properties": { - "browserTarget": { - "type": "string", - "description": "Target to extract from." - }, - "i18nFormat": { - "type": "string", - "description": "Output format for the generated file.", - "default": "xlf", - "enum": [ - "xmb", - "xlf", - "xlif", - "xliff", - "xlf2", - "xliff2" - ] - }, - "i18nLocale": { - "type": "string", - "description": "Specifies the source language of the application." - }, - "outputPath": { - "type": "string", - "description": "Path where output will be placed." - }, - "outFile": { - "type": "string", - "description": "Name of the file to output." - } - }, - "additionalProperties": false - }, - "karma": { - "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": { - "type": "string", - "description": "The name of the polyfills file." - }, - "assets": { - "type": "array", - "description": "List of static application assets.", - "default": [], - "items": { - "$ref": "#/definitions/targetOptions/definitions/karma/definitions/assetPattern" - } - }, - "scripts": { - "description": "Global scripts to be included in the build.", - "type": "array", - "default": [], - "items": { - "$ref": "#/definitions/targetOptions/definitions/karma/definitions/extraEntryPoint" - } - }, - "styles": { - "description": "Global styles to be included in the build.", - "type": "array", - "default": [], - "items": { - "$ref": "#/definitions/targetOptions/definitions/karma/definitions/extraEntryPoint" - } - }, - "stylePreprocessorOptions": { - "description": "Options to pass to style preprocessors", - "type": "object", - "properties": { - "includePaths": { - "description": "Paths to include. Paths will be resolved to project root.", - "type": "array", - "items": { - "type": "string" - }, - "default": [] - } - }, - "additionalProperties": false - }, - "environment": { - "type": "string", - "description": "Defines the build environment." - }, - "sourceMap": { - "type": "boolean", - "description": "Output sourcemaps.", - "default": true - }, - "progress": { - "type": "boolean", - "description": "Log progress to the console while building.", - "default": true - }, - "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." - }, - "preserveSymlinks": { - "type": "boolean", - "description": "Do not use the real path when resolving modules.", - "default": false - }, - "browsers": { - "type": "string", - "description": "Override which browsers tests are run against." - }, - "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 files with other 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": [] - } - }, - "additionalProperties": false, - "definitions": { - "assetPattern": { - "oneOf": [ - { - "type": "object", - "properties": { - "glob": { - "type": "string", - "description": "The pattern to match." - }, - "input": { - "type": "string", - "description": "The input path dir in which to apply 'glob'. Defaults to the project root." - }, - "output": { - "type": "string", - "description": "Absolute path within the output." - } - }, - "additionalProperties": false, - "required": [ - "glob", - "input", - "output" - ] - }, - { - "type": "string", - "description": "The file to include." - } - ] - }, - "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 - } - }, - "additionalProperties": false, - "required": [ - "input" - ] - }, - { - "type": "string", - "description": "The file to include." - } - ] - } - } - }, - "protractor": { - "description": "Protractor target options for Build Facade.", - "type": "object", - "properties": { - "protractorConfig": { - "type": "string", - "description": "The name of the Protractor configuration file." - }, - "devServerTarget": { - "type": "string", - "description": "Dev server target to run tests against." - }, - "specs": { - "type": "array", - "description": "Override specs in the protractor config.", - "default": [], - "items": { - "type": "string", - "description": "Spec name." - } - }, - "suite": { - "type": "string", - "description": "Override suite in the protractor config." - }, - "elementExplorer": { - "type": "boolean", - "description": "Start Protractor's Element Explorer for debugging.", - "default": false - }, - "webdriverUpdate": { - "type": "boolean", - "description": "Try to update webdriver.", - "default": true - }, - "serve": { - "type": "boolean", - "description": "Compile and Serve the app.", - "default": true - }, - "port": { - "type": "number", - "description": "The port to use to serve the application." - }, - "host": { - "type": "string", - "description": "Host to listen on.", - "default": "localhost" - }, - "baseUrl": { - "type": "string", - "description": "Base URL for protractor to connect to." - } - }, - "additionalProperties": false - }, - "server": { - "title": "Angular Webpack Architect Builder Schema", - "properties": { - "main": { - "type": "string", - "description": "The name of the main entry-point file." - }, - "tsConfig": { - "type": "string", - "default": "tsconfig.app.json", - "description": "The name of the TypeScript configuration file." - }, - "stylePreprocessorOptions": { - "description": "Options to pass to style preprocessors", - "type": "object", - "properties": { - "includePaths": { - "description": "Paths to include. Paths will be resolved to project root.", - "type": "array", - "items": { - "type": "string" - }, - "default": [] - } - }, - "additionalProperties": false - }, - "optimization": { - "type": "boolean", - "description": "Defines the optimization level of the build.", - "default": false - }, - "fileReplacements": { - "description": "Replace files with other files in the build.", - "type": "array", - "items": { - "$ref": "#/definitions/targetOptions/definitions/server/definitions/fileReplacement" - }, - "default": [] - }, - "outputPath": { - "type": "string", - "description": "Path where output will be placed." - }, - "sourceMap": { - "type": "boolean", - "description": "Output sourcemaps.", - "default": true - }, - "evalSourceMap": { - "type": "boolean", - "description": "Output in-file eval sourcemaps.", - "default": false - }, - "vendorChunk": { - "type": "boolean", - "description": "Use a separate bundle containing only vendor libraries.", - "default": true - }, - "commonChunk": { - "type": "boolean", - "description": "Use a separate bundle containing code used across multiple bundles.", - "default": true - }, - "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 - }, - "i18nFile": { - "type": "string", - "description": "Localization file to use for i18n." - }, - "i18nFormat": { - "type": "string", - "description": "Format of the localization file specified with --i18n-file." - }, - "i18nLocale": { - "type": "string", - "description": "Locale to use for i18n." - }, - "i18nMissingTranslation": { - "type": "string", - "description": "How to handle missing translations for i18n." - }, - "outputHashing": { - "type": "string", - "description": "Define the output filename cache-busting hashing mode.", - "default": "none", - "enum": [ - "none", - "all", - "media", - "bundles" - ] - }, - "deleteOutputPath": { - "type": "boolean", - "description": "delete-output-path", - "default": true - }, - "preserveSymlinks": { - "type": "boolean", - "description": "Do not use the real path when resolving modules.", - "default": false - }, - "extractLicenses": { - "type": "boolean", - "description": "Extract all licenses in a separate file, in the case of production builds only.", - "default": true - }, - "showCircularDependencies": { - "type": "boolean", - "description": "Show circular dependency warnings on builds.", - "default": true - }, - "namedChunks": { - "type": "boolean", - "description": "Use file name for lazy loaded chunks.", - "default": true - }, - "bundleDependencies": { - "type": "string", - "description": "Available on server platform only. Which external dependencies to bundle into the module. By default, all of node_modules will be kept as requires.", - "default": "none", - "enum": [ - "none", - "all" - ] - }, - "statsJson": { - "type": "boolean", - "description": "Generates a 'stats.json' file which can be analyzed using tools such as: #webpack-bundle-analyzer' or https://webpack.github.io/analyse .", - "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 with be discovered automatically.", - "type": "array", - "items": { - "type": "string" - }, - "default": [] - } - }, - "additionalProperties": false, - "definitions": { - "fileReplacement": { - "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" - ] - } - ] - } - - } - }, - "tslint": { - "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, codeFrame).", - "default": "prose", - "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 - } - } - } - } -} diff --git a/packages/angular/cli/lib/config/workspace-schema.json b/packages/angular/cli/lib/config/workspace-schema.json new file mode 100644 index 000000000000..0c551dc4fb14 --- /dev/null +++ b/packages/angular/cli/lib/config/workspace-schema.json @@ -0,0 +1,886 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "ng-cli://config/schema.json", + "title": "Angular CLI Workspace Configuration", + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "version": { + "$ref": "#/definitions/fileVersion" + }, + "cli": { + "$ref": "#/definitions/cliOptions" + }, + "schematics": { + "$ref": "#/definitions/schematicOptions" + }, + "newProjectRoot": { + "type": "string", + "description": "Path where new projects will be created." + }, + "projects": { + "type": "object", + "patternProperties": { + "^(?:@[a-zA-Z0-9._-]+/)?[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/project" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": ["version"], + "definitions": { + "cliOptions": { + "type": "object", + "properties": { + "schematicCollections": { + "type": "array", + "description": "The list of schematic collections to use.", + "items": { + "type": "string", + "uniqueItems": true + } + }, + "packageManager": { + "description": "Specify which package manager tool to use.", + "type": "string", + "enum": ["npm", "cnpm", "yarn", "pnpm", "bun"] + }, + "warnings": { + "description": "Control CLI specific console warnings", + "type": "object", + "properties": { + "versionMismatch": { + "description": "Show a warning when the global version is newer than the local one.", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "analytics": { + "type": ["boolean", "string"], + "description": "Share pseudonymous usage data with the Angular Team at Google." + }, + "cache": { + "description": "Control disk cache.", + "type": "object", + "properties": { + "environment": { + "description": "Configure in which environment disk cache is enabled.", + "type": "string", + "enum": ["local", "ci", "all"] + }, + "enabled": { + "description": "Configure whether disk caching is enabled.", + "type": "boolean" + }, + "path": { + "description": "Cache base path.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "cliGlobalOptions": { + "type": "object", + "properties": { + "schematicCollections": { + "type": "array", + "description": "The list of schematic collections to use.", + "items": { + "type": "string", + "uniqueItems": true + } + }, + "packageManager": { + "description": "Specify which package manager tool to use.", + "type": "string", + "enum": ["npm", "cnpm", "yarn", "pnpm", "bun"] + }, + "warnings": { + "description": "Control CLI specific console warnings", + "type": "object", + "properties": { + "versionMismatch": { + "description": "Show a warning when the global version is newer than the local one.", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "analytics": { + "type": ["boolean", "string"], + "description": "Share pseudonymous usage data with the Angular Team at Google." + }, + "completion": { + "type": "object", + "description": "Angular CLI completion settings.", + "properties": { + "prompted": { + "type": "boolean", + "description": "Whether the user has been prompted to add completion command prompt." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "schematicOptions": { + "type": "object", + "properties": { + "@schematics/angular:application": { + "$ref": "../../../../schematics/angular/application/schema.json" + }, + "@schematics/angular:class": { + "$ref": "../../../../schematics/angular/class/schema.json" + }, + "@schematics/angular:component": { + "$ref": "../../../../schematics/angular/component/schema.json" + }, + "@schematics/angular:directive": { + "$ref": "../../../../schematics/angular/directive/schema.json" + }, + "@schematics/angular:enum": { + "$ref": "../../../../schematics/angular/enum/schema.json" + }, + "@schematics/angular:guard": { + "$ref": "../../../../schematics/angular/guard/schema.json" + }, + "@schematics/angular:interceptor": { + "$ref": "../../../../schematics/angular/interceptor/schema.json" + }, + "@schematics/angular:interface": { + "$ref": "../../../../schematics/angular/interface/schema.json" + }, + "@schematics/angular:library": { + "$ref": "../../../../schematics/angular/library/schema.json" + }, + "@schematics/angular:pipe": { + "$ref": "../../../../schematics/angular/pipe/schema.json" + }, + "@schematics/angular:ng-new": { + "$ref": "../../../../schematics/angular/ng-new/schema.json" + }, + "@schematics/angular:resolver": { + "$ref": "../../../../schematics/angular/resolver/schema.json" + }, + "@schematics/angular:service": { + "$ref": "../../../../schematics/angular/service/schema.json" + }, + "@schematics/angular:web-worker": { + "$ref": "../../../../schematics/angular/web-worker/schema.json" + } + }, + "additionalProperties": true + }, + "fileVersion": { + "type": "integer", + "description": "File format version", + "minimum": 1 + }, + "project": { + "type": "object", + "properties": { + "cli": { + "schematicCollections": { + "type": "array", + "description": "The list of schematic collections to use.", + "items": { + "type": "string", + "uniqueItems": true + } + } + }, + "schematics": { + "$ref": "#/definitions/schematicOptions" + }, + "prefix": { + "type": "string", + "format": "html-selector", + "description": "The prefix to apply to generated selectors." + }, + "root": { + "type": "string", + "description": "Root of the project files." + }, + "i18n": { + "$ref": "#/definitions/project/definitions/i18n" + }, + "sourceRoot": { + "type": "string", + "description": "The root of the source files, assets and index.html file structure." + }, + "projectType": { + "type": "string", + "description": "Project type.", + "enum": ["application", "library"] + }, + "architect": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/project/definitions/target" + } + }, + "targets": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/project/definitions/target" + } + } + }, + "required": ["root", "projectType"], + "anyOf": [ + { + "required": ["architect"], + "not": { + "required": ["targets"] + } + }, + { + "required": ["targets"], + "not": { + "required": ["architect"] + } + }, + { + "not": { + "required": ["targets", "architect"] + } + } + ], + "additionalProperties": false, + "patternProperties": { + "^[a-z]{1,3}-.*": {} + }, + "definitions": { + "i18n": { + "description": "Project i18n options", + "type": "object", + "properties": { + "sourceLocale": { + "oneOf": [ + { + "type": "string", + "description": "Specifies the source locale of the application.", + "default": "en-US", + "$comment": "IETF BCP 47 language tag (simplified)", + "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})+)?$" + }, + { + "type": "object", + "description": "Localization options to use for the source locale.", + "properties": { + "code": { + "type": "string", + "description": "Specifies the locale code of the source locale.", + "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})+)?$" + }, + "baseHref": { + "type": "string", + "description": "Specifies the HTML base HREF for the locale. Defaults to the locale code if not provided." + }, + "subPath": { + "type": "string", + "description": "Defines the subpath for accessing this locale. It serves as the HTML base HREF and the directory name for the output. Defaults to the locale code if not specified.", + "pattern": "^[\\w-]*$" + } + }, + "anyOf": [ + { + "required": ["subPath"], + "not": { + "required": ["baseHref"] + } + }, + { + "required": ["baseHref"], + "not": { + "required": ["subPath"] + } + }, + { + "not": { + "required": ["baseHref", "subPath"] + } + } + ], + "additionalProperties": false + } + ] + }, + "locales": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[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})+)?$": { + "oneOf": [ + { + "type": "string", + "description": "Localization file to use for i18n." + }, + { + "type": "array", + "description": "Localization files to use for i18n.", + "items": { + "type": "string", + "uniqueItems": true + } + }, + { + "type": "object", + "description": "Localization options to use for the locale.", + "properties": { + "translation": { + "oneOf": [ + { + "type": "string", + "description": "Localization file to use for i18n." + }, + { + "type": "array", + "description": "Localization files to use for i18n.", + "items": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "baseHref": { + "type": "string", + "description": "Specifies the HTML base HREF for the locale. Defaults to the locale code if not provided." + }, + "subPath": { + "type": "string", + "description": "Defines the URL segment for accessing this locale. It serves as the HTML base HREF and the directory name for the output. Defaults to the locale code if not specified.", + "pattern": "^[\\w-]*$" + } + }, + "anyOf": [ + { + "required": ["subPath"], + "not": { + "required": ["baseHref"] + } + }, + { + "required": ["baseHref"], + "not": { + "required": ["subPath"] + } + }, + { + "not": { + "required": ["baseHref", "subPath"] + } + } + ], + "additionalProperties": false + } + ] + } + } + } + }, + "additionalProperties": false + }, + "target": { + "oneOf": [ + { + "$comment": "Extendable target with custom builder", + "type": "object", + "properties": { + "builder": { + "type": "string", + "description": "The builder used for this package.", + "not": { + "enum": [ + "@angular/build:application", + "@angular/build:dev-server", + "@angular/build:extract-i18n", + "@angular/build:karma", + "@angular/build:ng-packagr", + "@angular/build:unit-test", + "@angular-devkit/build-angular:application", + "@angular-devkit/build-angular:app-shell", + "@angular-devkit/build-angular:browser", + "@angular-devkit/build-angular:browser-esbuild", + "@angular-devkit/build-angular:dev-server", + "@angular-devkit/build-angular:extract-i18n", + "@angular-devkit/build-angular:karma", + "@angular-devkit/build-angular:ng-packagr", + "@angular-devkit/build-angular:prerender", + "@angular-devkit/build-angular:jest", + "@angular-devkit/build-angular:web-test-runner", + "@angular-devkit/build-angular:server", + "@angular-devkit/build-angular:ssr-dev-server" + ] + } + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "type": "object" + }, + "configurations": { + "type": "object", + "description": "A map of alternative target options.", + "additionalProperties": { + "type": "object" + } + } + }, + "additionalProperties": false, + "required": ["builder"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular/build:application" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular/build/src/builders/application/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular/build/src/builders/application/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:application" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular/build/src/builders/application/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular/build/src/builders/application/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:app-shell" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/app-shell/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/app-shell/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:browser" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/browser/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/browser/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:browser-esbuild" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/browser-esbuild/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/browser-esbuild/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular/build:dev-server" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular/build/src/builders/dev-server/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular/build/src/builders/dev-server/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:dev-server" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/dev-server/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/dev-server/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular/build:extract-i18n" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular/build/src/builders/extract-i18n/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular/build/src/builders/extract-i18n/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:extract-i18n" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/extract-i18n/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/extract-i18n/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular/build:unit-test" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular/build/src/builders/unit-test/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular/build/src/builders/unit-test/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular/build:karma" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular/build/src/builders/karma/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular/build/src/builders/karma/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:karma" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/karma/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/karma/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:jest" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/jest/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/jest/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:web-test-runner" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/web-test-runner/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/web-test-runner/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:prerender" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/prerender/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/prerender/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:ssr-dev-server" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/ssr-dev-server/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/ssr-dev-server/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:server" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/server/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/server/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular-devkit/build-angular:ng-packagr" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/ng-packagr/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular_devkit/build_angular/src/builders/ng-packagr/schema.json" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "builder": { + "const": "@angular/build:ng-packagr" + }, + "defaultConfiguration": { + "type": "string", + "description": "A default named configuration to use when a target configuration is not provided." + }, + "options": { + "$ref": "../../../../angular/build/src/builders/ng-packagr/schema.json" + }, + "configurations": { + "type": "object", + "additionalProperties": { + "$ref": "../../../../angular/build/src/builders/ng-packagr/schema.json" + } + } + } + } + ] + } + } + }, + "global": { + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "version": { + "$ref": "#/definitions/fileVersion" + }, + "cli": { + "$ref": "#/definitions/cliGlobalOptions" + }, + "schematics": { + "$ref": "#/definitions/schematicOptions" + } + }, + "required": ["version"] + } + } +} diff --git a/packages/angular/cli/lib/init.ts b/packages/angular/cli/lib/init.ts index 929bb1c2d4ea..cd324b6df69b 100644 --- a/packages/angular/cli/lib/init.ts +++ b/packages/angular/cli/lib/init.ts @@ -1,131 +1,147 @@ /** * @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 'symbol-observable'; -// symbol polyfill must go first -// tslint:disable-next-line:ordered-imports import-groups -import { tags, terminal } from '@angular-devkit/core'; -import { resolve } from '@angular-devkit/core/node'; -import * as fs from 'fs'; -import * as path from 'path'; -import { SemVer } from 'semver'; -import { isWarningEnabled } from '../utilities/config'; -const packageJson = require('../package.json'); +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import * as path from 'node:path'; +import { SemVer, major } from 'semver'; +import { colors } from '../src/utilities/color'; +import { isWarningEnabled } from '../src/utilities/config'; +import { disableVersionCheck } from '../src/utilities/environment-options'; +import { VERSION } from '../src/utilities/version'; -function _fromPackageJson(cwd?: string) { - cwd = cwd || process.cwd(); +/** + * Angular CLI versions prior to v14 may not exit correctly if not forcibly exited + * via `process.exit()`. When bootstrapping, `forceExit` will be set to `true` + * if the local CLI version is less than v14 to prevent the CLI from hanging on + * exit in those cases. + */ +let forceExit = false; + +(async (): Promise => { + /** + * Disable Browserslist old data warning as otherwise with every release we'd need to update this dependency + * which is cumbersome considering we pin versions and the warning is not user actionable. + * `Browserslist: caniuse-lite is outdated. Please run next command `npm update` + * See: https://github.com/browserslist/browserslist/blob/819c4337456996d19db6ba953014579329e9c6e1/node.js#L324 + */ + process.env.BROWSERSLIST_IGNORE_OLD_DATA = '1'; + const rawCommandName = process.argv[2]; + + /** + * Disable CLI version mismatch checks and forces usage of the invoked CLI + * instead of invoking the local installed version. + * + * When running `ng new` always favor the global version. As in some + * cases orphan `node_modules` would cause the non global CLI to be used. + * @see: https://github.com/angular/angular-cli/issues/14603 + */ + if (disableVersionCheck || rawCommandName === 'new') { + return (await import('./cli')).default; + } - do { - const packageJsonPath = path.join(cwd, 'node_modules/@angular/cli/package.json'); - if (fs.existsSync(packageJsonPath)) { - const content = fs.readFileSync(packageJsonPath, 'utf-8'); - if (content) { - const json = JSON.parse(content); - if (json['version']) { - return new SemVer(json['version']); - } + let cli; + + try { + // No error implies a projectLocalCli, which will load whatever + // version of ng-cli you have installed in a local package.json + const cwdRequire = createRequire(process.cwd() + '/'); + const projectLocalCli = cwdRequire.resolve('@angular/cli'); + cli = await import(projectLocalCli); + + const globalVersion = new SemVer(VERSION.full); + + // Older versions might not have the VERSION export + let localVersion = cli.VERSION?.full; + if (!localVersion) { + try { + const localPackageJson = await readFile( + path.join(path.dirname(projectLocalCli), '../../package.json'), + 'utf-8', + ); + localVersion = (JSON.parse(localPackageJson) as { version: string }).version; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Version mismatch check skipped. Unable to retrieve local version: ' + error); } } - // Check the parent. - cwd = path.dirname(cwd); - } while (cwd != path.dirname(cwd)); + // Ensure older versions of the CLI fully exit + const localMajorVersion = major(localVersion); + if (localMajorVersion > 0 && localMajorVersion < 14) { + forceExit = true; - return null; -} - - -// Check if we need to profile this CLI run. -if (process.env['NG_CLI_PROFILING']) { - const profiler = require('v8-profiler'); // tslint:disable-line:no-implicit-dependencies - profiler.startProfiling(); - const exitHandler = (options: { cleanup?: boolean, exit?: boolean }) => { - if (options.cleanup) { - const cpuProfile = profiler.stopProfiling(); - fs.writeFileSync( - path.resolve(process.cwd(), process.env.NG_CLI_PROFILING || '') + '.cpuprofile', - JSON.stringify(cpuProfile), - ); + // Versions prior to 14 didn't implement completion command. + if (rawCommandName === 'completion') { + return null; + } } - if (options.exit) { - process.exit(); + let isGlobalGreater = false; + try { + isGlobalGreater = localVersion > 0 && globalVersion.compare(localVersion) > 0; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Version mismatch check skipped. Unable to compare local version: ' + error); } - }; - - process.on('exit', () => exitHandler({ cleanup: true })); - process.on('SIGINT', () => exitHandler({ exit: true })); - process.on('uncaughtException', () => exitHandler({ exit: true })); -} - -let cli; -try { - const projectLocalCli = resolve( - '@angular/cli', - { - checkGlobal: false, - basedir: process.cwd(), - preserveSymlinks: true, - }, - ); - // This was run from a global, check local version. - const globalVersion = new SemVer(packageJson['version']); - let localVersion; - let shouldWarn = false; - - try { - localVersion = _fromPackageJson(); - shouldWarn = localVersion != null && globalVersion.compare(localVersion) > 0; - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - shouldWarn = true; - } - - if (shouldWarn && isWarningEnabled('versionMismatch')) { - const warning = terminal.yellow(tags.stripIndents` - Your global Angular CLI version (${globalVersion}) is greater than your local - version (${localVersion}). The local Angular CLI version is used. - - To disable this warning use "ng config -g cli.warnings.versionMismatch false". - `); - // Don't show warning colorised on `ng completion` - if (process.argv[2] !== 'completion') { - // eslint-disable-next-line no-console - console.log(warning); - } else { - // eslint-disable-next-line no-console - console.error(warning); - process.exit(1); + // When using the completion command, don't show the warning as otherwise this will break completion. + if ( + isGlobalGreater && + rawCommandName !== '--get-yargs-completions' && + rawCommandName !== 'completion' + ) { + // If using the update command and the global version is greater, use the newer update command + // This allows improvements in update to be used in older versions that do not have bootstrapping + if ( + rawCommandName === 'update' && + cli.VERSION && + cli.VERSION.major - globalVersion.major <= 1 + ) { + cli = await import('./cli'); + } else if (await isWarningEnabled('versionMismatch')) { + // Otherwise, use local version and warn if global is newer than local + const warning = + `Your global Angular CLI version (${globalVersion}) is greater than your local ` + + `version (${localVersion}). The local Angular CLI version is used.\n\n` + + 'To disable this warning use "ng config -g cli.warnings.versionMismatch false".'; + + // eslint-disable-next-line no-console + console.error(colors.yellow(warning)); + } } + } catch { + // If there is an error, resolve could not find the ng-cli + // library from a package.json. Instead, include it from a relative + // path to this script file (which is likely a globally installed + // npm package). Most common cause for hitting this is `ng new` + cli = await import('./cli'); } - // No error implies a projectLocalCli, which will load whatever - // version of ng-cli you have installed in a local package.json - cli = require(projectLocalCli); -} catch { - // If there is an error, resolve could not find the ng-cli - // library from a package.json. Instead, include it from a relative - // path to this script file (which is likely a globally installed - // npm package). Most common cause for hitting this is `ng new` - cli = require('./cli'); -} - -if ('default' in cli) { - cli = cli['default']; -} + if ('default' in cli) { + cli = cli['default']; + } -cli({ cliArgs: process.argv.slice(2) }) - .then((exitCode: number) => { - process.exit(exitCode); + return cli; +})() + .then((cli) => + cli?.({ + cliArgs: process.argv.slice(2), + }), + ) + .then((exitCode = 0) => { + if (forceExit) { + process.exit(exitCode); + } + process.exitCode = exitCode; }) .catch((err: Error) => { - console.log('Unknown error: ' + err.toString()); + // eslint-disable-next-line no-console + console.error('Unknown error: ' + err.toString()); process.exit(127); }); diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts deleted file mode 100644 index 58a811eed629..000000000000 --- a/packages/angular/cli/models/architect-command.ts +++ /dev/null @@ -1,289 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { - Architect, - BuildEvent, - BuilderDescription, - TargetSpecifier, -} from '@angular-devkit/architect'; -import { JsonObject, experimental, schema, strings } from '@angular-devkit/core'; -import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node'; -import { of } from 'rxjs'; -import { from } from 'rxjs'; -import { concatMap, map, tap, toArray } from 'rxjs/operators'; -import { Command, Option } from './command'; -import { WorkspaceLoader } from './workspace-loader'; - -export interface ProjectAndConfigurationOptions { - project?: string; - configuration?: string; - prod: boolean; -} - -export interface TargetOptions { - target?: string; -} - -export type ArchitectCommandOptions = ProjectAndConfigurationOptions & TargetOptions & JsonObject; - -export abstract class ArchitectCommand extends Command { - - private _host = new NodeJsSyncHost(); - private _architect: Architect; - private _workspace: experimental.workspace.Workspace; - private _logger = createConsoleLogger(); - // If this command supports running multiple targets. - protected multiTarget = false; - - readonly Options: Option[] = [{ - name: 'configuration', - description: 'The configuration', - type: String, - aliases: ['c'], - }]; - - readonly arguments = ['project']; - - target: string | undefined; - - public async initialize(options: ArchitectCommandOptions): Promise { - return this._loadWorkspaceAndArchitect().pipe( - concatMap(() => { - const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options); - - if (this.target && !targetSpec.project) { - const projects = this.getProjectNamesByTarget(this.target); - - if (projects.length === 1) { - // If there is a single target, use it to parse overrides. - targetSpec.project = projects[0]; - } else { - // Multiple targets can have different, incompatible options. - // We only lookup options for single targets. - return of(null); - } - } - - if (!targetSpec.project || !targetSpec.target) { - throw new Error('Cannot determine project or target for Architect command.'); - } - - const builderConfig = this._architect.getBuilderConfiguration(targetSpec); - - return this._architect.getBuilderDescription(builderConfig).pipe( - tap(builderDesc => { this.mapArchitectOptions(builderDesc.schema); }), - ); - }), - ).toPromise() - .then(() => { }); - } - - public validate(options: ArchitectCommandOptions) { - if (!options.project && this.target) { - const projectNames = this.getProjectNamesByTarget(this.target); - const { overrides } = this._makeTargetSpecifier(options); - if (projectNames.length > 1 && Object.keys(overrides || {}).length > 0) { - throw new Error('Architect commands with multiple targets cannot specify overrides.' - + `'${this.target}' would be run on the following projects: ${projectNames.join()}`); - } - } - - return true; - } - - protected mapArchitectOptions(schema: any) { - const properties = schema.properties; - const keys = Object.keys(properties); - keys - .map(key => ({ ...properties[key], ...{ name: strings.dasherize(key) } })) - .map(opt => { - let type; - const schematicType = opt.type; - switch (opt.type) { - case 'string': - type = String; - break; - case 'boolean': - type = Boolean; - break; - case 'integer': - case 'number': - type = Number; - break; - - // Ignore arrays / objects. - default: - return null; - } - let aliases: string[] = []; - if (opt.alias) { - aliases = [...aliases, opt.alias]; - } - if (opt.aliases) { - aliases = [...aliases, ...opt.aliases]; - } - - const schematicDefault = opt.default; - - return { - ...opt, - aliases, - type, - schematicType, - default: undefined, // do not carry over schematics defaults - schematicDefault, - hidden: opt.visible === false, - }; - }) - .filter(x => x) - .forEach(option => this.options.push(option)); - } - - protected prodOption: Option = { - name: 'prod', - description: 'Flag to set configuration to "prod".', - type: Boolean, - }; - - protected configurationOption: Option = { - name: 'configuration', - description: 'Specify the configuration to use.', - type: String, - aliases: ['c'], - }; - - protected async runArchitectTarget(options: ArchitectCommandOptions): Promise { - const targetSpec = this._makeTargetSpecifier(options); - - const runSingleTarget = (targetSpec: TargetSpecifier) => this._architect.run( - this._architect.getBuilderConfiguration(targetSpec), - { logger: this._logger }, - ).pipe( - map((buildEvent: BuildEvent) => buildEvent.success ? 0 : 1), - ); - - try { - if (!targetSpec.project && this.target) { - // This runs each target sequentially. - // Running them in parallel would jumble the log messages. - return await from(this.getProjectNamesByTarget(this.target)).pipe( - concatMap(project => runSingleTarget({ ...targetSpec, project })), - toArray(), - map(results => results.every(res => res === 0) ? 0 : 1), - ) - .toPromise(); - } else { - return await runSingleTarget(targetSpec).toPromise(); - } - } catch (e) { - if (e instanceof schema.SchemaValidationException) { - const newErrors: schema.SchemaValidatorError[] = []; - for (const schemaError of e.errors) { - if (schemaError.keyword === 'additionalProperties') { - const unknownProperty = schemaError.params.additionalProperty; - if (unknownProperty in options) { - const dashes = unknownProperty.length === 1 ? '-' : '--'; - this.logger.fatal(`Unknown option: '${dashes}${unknownProperty}'`); - continue; - } - } - newErrors.push(schemaError); - } - - if (newErrors.length > 0) { - this.logger.error(new schema.SchemaValidationException(newErrors).message); - } - - return 1; - } else { - throw e; - } - } - } - - private getProjectNamesByTarget(targetName: string): string[] { - const allProjectsForTargetName = this._workspace.listProjectNames().map(projectName => - this._architect.listProjectTargets(projectName).includes(targetName) ? projectName : null, - ).filter(x => !!x) as string[]; - - if (this.multiTarget) { - // For multi target commands, we always list all projects that have the target. - return allProjectsForTargetName; - } else { - // For single target commands, we try try the default project project first, - // then the full list if it has a single project, then error out. - const maybeDefaultProject = this._workspace.getDefaultProjectName(); - if (maybeDefaultProject && allProjectsForTargetName.includes(maybeDefaultProject)) { - return [maybeDefaultProject]; - } - - if (allProjectsForTargetName.length === 1) { - return allProjectsForTargetName; - } - - throw new Error(`Could not determine a single project for the '${targetName}' target.`); - } - } - - private _loadWorkspaceAndArchitect() { - const workspaceLoader = new WorkspaceLoader(this._host); - - return workspaceLoader.loadWorkspace(this.project.root).pipe( - tap((workspace: experimental.workspace.Workspace) => this._workspace = workspace), - concatMap((workspace: experimental.workspace.Workspace) => { - return new Architect(workspace).loadArchitect(); - }), - tap((architect: Architect) => this._architect = architect), - ); - } - - private _makeTargetSpecifier(options: ArchitectCommandOptions): TargetSpecifier { - let project, target, configuration, overrides; - - if (options.target) { - [project, target, configuration] = options.target.split(':'); - - overrides = { ...options }; - delete overrides.target; - - if (overrides.configuration) { - configuration = overrides.configuration; - delete overrides.configuration; - } - } else { - project = options.project; - target = this.target; - configuration = options.configuration; - if (!configuration && options.prod) { - configuration = 'production'; - } - - overrides = { ...options }; - - delete overrides.configuration; - delete overrides.prod; - delete overrides.project; - } - - if (!project) { - project = ''; - } - if (!target) { - target = ''; - } - - return { - project, - configuration, - target, - overrides, - }; - } -} diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts deleted file mode 100644 index 6372236353f8..000000000000 --- a/packages/angular/cli/models/command-runner.ts +++ /dev/null @@ -1,307 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { logging, strings as coreStrings, tags } from '@angular-devkit/core'; -import * as yargsParser from 'yargs-parser'; -import { - ArgumentStrategy, - CommandConstructor, - CommandContext, - CommandScope, - Option, -} from '../models/command'; -import { insideProject } from '../utilities/project'; - - -export interface CommandMap { - [key: string]: CommandConstructor; -} - -// Based off https://en.wikipedia.org/wiki/Levenshtein_distance -// No optimization, really. -function levenshtein(a: string, b: string): number { - /* base case: empty strings */ - if (a.length == 0) { - return b.length; - } - if (b.length == 0) { - return a.length; - } - - // Test if last characters of the strings match. - const cost = a[a.length - 1] == b[b.length - 1] ? 0 : 1; - - /* return minimum of delete char from s, delete char from t, and delete char from both */ - return Math.min( - levenshtein(a.slice(0, -1), b) + 1, - levenshtein(a, b.slice(0, -1)) + 1, - levenshtein(a.slice(0, -1), b.slice(0, -1)) + cost, - ); -} - -/** - * Run a command. - * @param commandMap Map of available commands. - * @param args Raw unparsed arguments. - * @param logger The logger to use. - * @param context Execution context. - */ -export async function runCommand(commandMap: CommandMap, - args: string[], - logger: logging.Logger, - context: CommandContext): Promise { - - // if not args supplied, just run the help command. - if (!args || args.length === 0) { - args = ['help']; - } - const rawOptions = yargsParser(args, { alias: { help: ['h'] }, boolean: [ 'help' ] }); - let commandName = rawOptions._[0]; - - // remove the command name - rawOptions._ = rawOptions._.slice(1); - const executionScope = insideProject() - ? CommandScope.inProject - : CommandScope.outsideProject; - - let Cmd: CommandConstructor | null; - Cmd = findCommand(commandMap, commandName); - - if (!Cmd && !commandName && (rawOptions.v || rawOptions.version)) { - commandName = 'version'; - Cmd = findCommand(commandMap, commandName); - } - - if (!Cmd && rawOptions.help) { - commandName = 'help'; - Cmd = findCommand(commandMap, commandName); - } - - if (!Cmd) { - const commandsDistance = {} as { [name: string]: number }; - const allCommands = listAllCommandNames(commandMap).sort((a, b) => { - if (!(a in commandsDistance)) { - commandsDistance[a] = levenshtein(a, commandName); - } - if (!(b in commandsDistance)) { - commandsDistance[b] = levenshtein(b, commandName); - } - - return commandsDistance[a] - commandsDistance[b]; - }); - - logger.error(tags.stripIndent` - The specified command ("${commandName}") is invalid. For a list of available options, - run "ng help". - - Did you mean "${allCommands[0]}"? - `); - - return 1; - } - - const command = new Cmd(context, logger); - - args = await command.initializeRaw(args); - let options = parseOptions(args, command.options, command.arguments, command.argStrategy); - await command.initialize(options); - options = parseOptions(args, command.options, command.arguments, command.argStrategy); - if (commandName === 'help') { - options.commandMap = commandMap; - } - - if (options.help) { - command.printHelp(options); - - return; - } else { - if (Cmd.scope !== undefined && Cmd.scope !== CommandScope.everywhere) { - if (Cmd.scope !== executionScope) { - let errorMessage; - if (Cmd.scope === CommandScope.inProject) { - errorMessage = `This command can only be run inside of a CLI project.`; - } else { - errorMessage = `This command can not be run inside of a CLI project.`; - } - logger.fatal(errorMessage); - - return 1; - } - - if (Cmd.scope === CommandScope.inProject) { - if (!context.project.configFile) { - logger.fatal('Invalid project: missing workspace file.'); - - return 1; - } - - if (['.angular-cli.json', 'angular-cli.json'].includes(context.project.configFile)) { - // -------------------------------------------------------------------------------- - // If changing this message, please update the same message in - // `packages/@angular/cli/bin/ng-update-message.js` - const message = tags.stripIndent` - The Angular CLI configuration format has been changed, and your existing configuration - can be updated automatically by running the following command: - - ng update @angular/cli - `; - - logger.warn(message); - - return 1; - } - } - } - - delete options.h; - delete options.help; - - const isValid = await command.validate(options); - if (!isValid) { - logger.fatal(`Validation error. Invalid command`); - - return 1; - } - - return command.run(options); - } -} - -export function parseOptions( - args: string[], - cmdOpts: Option[], - commandArguments: string[], - argStrategy: ArgumentStrategy, -): T { - const parser = yargsParser; - - const aliases = cmdOpts.concat() - .filter(o => o.aliases && o.aliases.length > 0) - .reduce((aliases: any, opt: Option) => { - aliases[opt.name] = (opt.aliases || []) - .filter(a => a.length === 1); - - return aliases; - }, {}); - - const booleans = cmdOpts - .filter(o => o.type && o.type === Boolean) - .map(o => o.name); - - const defaults = cmdOpts - .filter(o => o.default !== undefined || booleans.indexOf(o.name) !== -1) - .reduce((defaults: any, opt: Option) => { - defaults[opt.name] = opt.default; - - return defaults; - }, {}); - - const strings = cmdOpts - .filter(o => o.type === String) - .map(o => o.name); - - const numbers = cmdOpts - .filter(o => o.type === Number) - .map(o => o.name); - - - aliases.help = ['h']; - booleans.push('help'); - - const yargsOptions = { - alias: aliases, - boolean: booleans, - default: defaults, - string: strings, - number: numbers, - }; - - const parsedOptions = parser(args, yargsOptions); - - // Remove aliases. - cmdOpts - .filter(o => o.aliases && o.aliases.length > 0) - .map(o => o.aliases) - .reduce((allAliases: any, aliases: string[]) => { - return allAliases.concat([...aliases]); - }, []) - .forEach((alias: string) => { - delete parsedOptions[alias]; - }); - - // Remove undefined booleans - booleans - .filter(b => parsedOptions[b] === undefined) - .map(b => coreStrings.camelize(b)) - .forEach(b => delete parsedOptions[b]); - - // remove options with dashes. - Object.keys(parsedOptions) - .filter(key => key.indexOf('-') !== -1) - .forEach(key => delete parsedOptions[key]); - - // remove the command name - parsedOptions._ = parsedOptions._.slice(1); - - switch (argStrategy) { - case ArgumentStrategy.MapToOptions: - parsedOptions._.forEach((value: string, index: number) => { - const arg = commandArguments[index]; - if (arg) { - parsedOptions[arg] = value; - } - }); - - delete parsedOptions._; - break; - } - - return parsedOptions; -} - -// Find a command. -function findCommand(map: CommandMap, name: string): CommandConstructor | null { - let Cmd: CommandConstructor = map[name]; - - if (!Cmd) { - // find command via aliases - Cmd = Object.keys(map) - .filter(key => { - if (!map[key].aliases) { - return false; - } - const foundAlias = map[key].aliases - .filter((alias: string) => alias === name); - - return foundAlias.length > 0; - }) - .map((key) => { - return map[key]; - })[0]; - } - - if (!Cmd) { - return null; - } - - return Cmd; -} - -function listAllCommandNames(map: CommandMap): string[] { - return Object.keys(map).concat( - Object.keys(map) - .reduce((acc, key) => { - if (!map[key].aliases) { - return acc; - } - - return acc.concat(map[key].aliases); - }, [] as string[]), - ); -} diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts deleted file mode 100644 index ab59be25af9d..000000000000 --- a/packages/angular/cli/models/command.ts +++ /dev/null @@ -1,113 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { logging, terminal } from '@angular-devkit/core'; - -export interface CommandConstructor { - new(context: CommandContext, logger: logging.Logger): Command; - readonly name: string; - aliases: string[]; - scope: CommandScope; -} - -export enum CommandScope { - everywhere, - inProject, - outsideProject, -} - -export enum ArgumentStrategy { - MapToOptions, - Nothing, -} - -export abstract class Command { - protected _rawArgs: string[]; - public allowMissingWorkspace = false; - - constructor(context: CommandContext, logger: logging.Logger) { - this.logger = logger; - if (context) { - this.project = context.project; - } - } - - async initializeRaw(args: string[]): Promise { - this._rawArgs = args; - - return args; - } - async initialize(_options: any): Promise { - return; - } - - validate(_options: T): boolean | Promise { - return true; - } - - printHelp(_options: T): void { - this.printHelpUsage(this.name, this.arguments, this.options); - this.printHelpOptions(this.options); - } - - protected printHelpUsage(name: string, args: string[], options: Option[]) { - const argDisplay = args && args.length > 0 - ? ' ' + args.map(a => `<${a}>`).join(' ') - : ''; - const optionsDisplay = options && options.length > 0 - ? ` [options]` - : ``; - this.logger.info(`usage: ng ${name}${argDisplay}${optionsDisplay}`); - } - - protected printHelpOptions(options: Option[]) { - if (options && this.options.length > 0) { - this.logger.info(`options:`); - this.options - .filter(o => !o.hidden) - .sort((a, b) => a.name >= b.name ? 1 : -1) - .forEach(o => { - const aliases = o.aliases && o.aliases.length > 0 - ? '(' + o.aliases.map(a => `-${a}`).join(' ') + ')' - : ''; - this.logger.info(` ${terminal.cyan('--' + o.name)} ${aliases}`); - this.logger.info(` ${o.description}`); - }); - } - } - - abstract run(options: T): number | void | Promise; - abstract readonly name: string; - abstract readonly description: string; - abstract readonly arguments: string[]; - abstract readonly options: Option[]; - public argStrategy = ArgumentStrategy.MapToOptions; - public hidden = false; - public unknown = false; - public static scope: CommandScope = CommandScope.everywhere; - public static aliases: string[] = []; - protected readonly logger: logging.Logger; - protected readonly project: any; -} - -export interface CommandContext { - project: any; -} - -export abstract class Option { - abstract readonly name: string; - abstract readonly description: string; - readonly default?: string | number | boolean; - readonly required?: boolean; - abstract readonly aliases?: string[]; - abstract readonly type: any; - readonly format?: string; - readonly values?: any[]; - readonly hidden?: boolean = false; -} diff --git a/packages/angular/cli/models/error.ts b/packages/angular/cli/models/error.ts deleted file mode 100644 index 5d9d323ed103..000000000000 --- a/packages/angular/cli/models/error.ts +++ /dev/null @@ -1,19 +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 class NgToolkitError extends Error { - constructor(message?: string) { - super(); - - if (message) { - this.message = message; - } else { - this.message = this.constructor.name; - } - } -} diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts deleted file mode 100644 index 11d272e7496a..000000000000 --- a/packages/angular/cli/models/schematic-command.ts +++ /dev/null @@ -1,388 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { JsonObject, experimental } from '@angular-devkit/core'; -import { normalize, strings, tags, terminal, virtualFs } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { DryRunEvent, UnsuccessfulWorkflowExecution } from '@angular-devkit/schematics'; -import { NodeWorkflow } from '@angular-devkit/schematics/tools'; -import { take } from 'rxjs/operators'; -import { WorkspaceLoader } from '../models/workspace-loader'; -import { getDefaultSchematicCollection, getPackageManager } from '../utilities/config'; -import { getSchematicDefaults } from '../utilities/config'; -import { getCollection, getSchematic } from '../utilities/schematics'; -import { ArgumentStrategy, Command, Option } from './command'; - -export interface CoreSchematicOptions { - dryRun: boolean; - force: boolean; -} - -export interface RunSchematicOptions { - collectionName: string; - schematicName: string; - schematicOptions: any; - debug?: boolean; - dryRun: boolean; - force: boolean; - showNothingDone?: boolean; -} - -export interface GetOptionsOptions { - collectionName: string; - schematicName: string; -} - -export interface GetOptionsResult { - options: Option[]; - arguments: Option[]; -} - -export abstract class SchematicCommand extends Command { - readonly options: Option[] = []; - readonly allowPrivateSchematics: boolean = false; - private _host = new NodeJsSyncHost(); - private _workspace: experimental.workspace.Workspace; - private _deAliasedName: string; - private _originalOptions: Option[]; - argStrategy = ArgumentStrategy.Nothing; - - protected readonly coreOptions: Option[] = [ - { - name: 'dryRun', - type: Boolean, - default: false, - aliases: ['d'], - description: 'Run through without making any changes.', - }, - { - name: 'force', - type: Boolean, - default: false, - aliases: ['f'], - description: 'Forces overwriting of files.', - }]; - - readonly arguments = ['project']; - - public async initialize(_options: any) { - this._loadWorkspace(); - } - - protected setPathOptions(options: any, workingDir: string): any { - if (workingDir === '') { - return {}; - } - - return this.options - .filter(o => o.format === 'path') - .map(o => o.name) - .filter(name => options[name] === undefined) - .reduce((acc: any, curr) => { - acc[curr] = workingDir; - - return acc; - }, {}); - } - - protected runSchematic(options: RunSchematicOptions) { - const { collectionName, schematicName, debug, force, dryRun } = options; - let schematicOptions = this.removeCoreOptions(options.schematicOptions); - let nothingDone = true; - let loggingQueue: string[] = []; - let error = false; - const fsHost = new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(this.project.root)); - const workflow = new NodeWorkflow( - fsHost as any, - { - force, - dryRun, - packageManager: getPackageManager(), - root: this.project.root, - }, - ); - - const workingDir = process.cwd().replace(this.project.root, '').replace(/\\/g, '/'); - const pathOptions = this.setPathOptions(schematicOptions, workingDir); - schematicOptions = { ...schematicOptions, ...pathOptions }; - const defaultOptions = this.readDefaults(collectionName, schematicName, schematicOptions); - schematicOptions = { ...schematicOptions, ...defaultOptions }; - - // Pass the rest of the arguments as the smart default "argv". Then delete it. - // Removing the first item which is the schematic name. - const rawArgs = schematicOptions._; - workflow.registry.addSmartDefaultProvider('argv', (schema: JsonObject) => { - if ('index' in schema) { - return rawArgs[Number(schema['index'])]; - } else { - return rawArgs; - } - }); - delete schematicOptions._; - - workflow.registry.addSmartDefaultProvider('projectName', (_schema: JsonObject) => { - if (this._workspace) { - try { - return this._workspace.getProjectByPath(normalize(process.cwd())) - || this._workspace.getDefaultProjectName(); - } catch (e) { - if (e instanceof experimental.workspace.AmbiguousProjectPathException) { - this.logger.warn(tags.oneLine` - Two or more projects are using identical roots. - Unable to determine project using current working directory. - Using default workspace project instead. - `); - - return this._workspace.getDefaultProjectName(); - } - throw e; - } - } - - return undefined; - }); - - workflow.reporter.subscribe((event: DryRunEvent) => { - nothingDone = false; - - // Strip leading slash to prevent confusion. - const eventPath = event.path.startsWith('/') ? event.path.substr(1) : event.path; - - switch (event.kind) { - case 'error': - error = true; - const desc = event.description == 'alreadyExist' ? 'already exists' : 'does not exist.'; - this.logger.warn(`ERROR! ${eventPath} ${desc}.`); - break; - case 'update': - loggingQueue.push(tags.oneLine` - ${terminal.white('UPDATE')} ${eventPath} (${event.content.length} bytes) - `); - break; - case 'create': - loggingQueue.push(tags.oneLine` - ${terminal.green('CREATE')} ${eventPath} (${event.content.length} bytes) - `); - break; - case 'delete': - loggingQueue.push(`${terminal.yellow('DELETE')} ${eventPath}`); - break; - case 'rename': - loggingQueue.push(`${terminal.blue('RENAME')} ${eventPath} => ${event.to}`); - break; - } - }); - - workflow.lifeCycle.subscribe(event => { - if (event.kind == 'end' || event.kind == 'post-tasks-start') { - if (!error) { - // Output the logging queue, no error happened. - loggingQueue.forEach(log => this.logger.info(log)); - } - - loggingQueue = []; - error = false; - } - }); - - return new Promise((resolve) => { - workflow.execute({ - collection: collectionName, - schematic: schematicName, - options: schematicOptions, - debug: debug, - logger: this.logger as any, - allowPrivate: this.allowPrivateSchematics, - }) - .subscribe({ - error: (err: Error) => { - // In case the workflow was not successful, show an appropriate error message. - if (err instanceof UnsuccessfulWorkflowExecution) { - // "See above" because we already printed the error. - this.logger.fatal('The Schematic workflow failed. See above.'); - } else if (debug) { - this.logger.fatal(`An error occured:\n${err.message}\n${err.stack}`); - } else { - this.logger.fatal(err.message); - } - - resolve(1); - }, - complete: () => { - const showNothingDone = !(options.showNothingDone === false); - if (nothingDone && showNothingDone) { - this.logger.info('Nothing to be done.'); - } - if (dryRun) { - this.logger.warn(`\nNOTE: Run with "dry run" no changes were made.`); - } - resolve(); - }, - }); - }); - } - - protected removeCoreOptions(options: any): any { - const opts = Object.assign({}, options); - if (this._originalOptions.find(option => option.name == 'dryRun')) { - delete opts.dryRun; - } - if (this._originalOptions.find(option => option.name == 'force')) { - delete opts.force; - } - if (this._originalOptions.find(option => option.name == 'debug')) { - delete opts.debug; - } - - return opts; - } - - protected getOptions(options: GetOptionsOptions): Promise { - // Make a copy. - this._originalOptions = [...this.options]; - - const collectionName = options.collectionName || getDefaultSchematicCollection(); - - const collection = getCollection(collectionName); - - const schematic = getSchematic(collection, options.schematicName, this.allowPrivateSchematics); - this._deAliasedName = schematic.description.name; - - if (!schematic.description.schemaJson) { - return Promise.resolve({ - options: [], - arguments: [], - }); - } - - const properties = schematic.description.schemaJson.properties; - const keys = Object.keys(properties); - const availableOptions = keys - .map(key => ({ ...properties[key], ...{ name: strings.dasherize(key) } })) - .map(opt => { - let type; - const schematicType = opt.type; - switch (opt.type) { - case 'string': - type = String; - break; - case 'boolean': - type = Boolean; - break; - case 'integer': - case 'number': - type = Number; - break; - - // Ignore arrays / objects. - default: - return null; - } - let aliases: string[] = []; - if (opt.alias) { - aliases = [...aliases, opt.alias]; - } - if (opt.aliases) { - aliases = [...aliases, ...opt.aliases]; - } - const schematicDefault = opt.default; - - return { - ...opt, - aliases, - type, - schematicType, - default: undefined, // do not carry over schematics defaults - schematicDefault, - hidden: opt.visible === false, - }; - }) - .filter(x => x); - - const schematicOptions = availableOptions - .filter(opt => opt.$default === undefined || opt.$default.$source !== 'argv'); - - const schematicArguments = availableOptions - .filter(opt => opt.$default !== undefined && opt.$default.$source === 'argv') - .sort((a, b) => { - if (a.$default.index === undefined) { - return 1; - } - if (b.$default.index === undefined) { - return -1; - } - if (a.$default.index == b.$default.index) { - return 0; - } else if (a.$default.index > b.$default.index) { - return 1; - } else { - return -1; - } - }); - - return Promise.resolve({ - options: schematicOptions, - arguments: schematicArguments, - }); - } - - private _loadWorkspace() { - if (this._workspace) { - return; - } - const workspaceLoader = new WorkspaceLoader(this._host); - - try { - workspaceLoader.loadWorkspace(this.project.root).pipe(take(1)) - .subscribe( - (workspace: experimental.workspace.Workspace) => this._workspace = workspace, - (err: Error) => { - if (!this.allowMissingWorkspace) { - // Ignore missing workspace - throw err; - } - }, - ); - } catch (err) { - if (!this.allowMissingWorkspace) { - // Ignore missing workspace - throw err; - } - } - } - - private _cleanDefaults(defaults: T, undefinedOptions: string[]): T { - (Object.keys(defaults) as K[]) - .filter(key => !undefinedOptions.map(strings.camelize).includes(key as string)) - .forEach(key => { - delete defaults[key]; - }); - - return defaults; - } - - private readDefaults(collectionName: string, schematicName: string, options: any): {} { - if (this._deAliasedName) { - schematicName = this._deAliasedName; - } - - const projectName = options.project; - const defaults = getSchematicDefaults(collectionName, schematicName, projectName); - - // Get list of all undefined options. - const undefinedOptions = this.options - .filter(o => options[o.name] === undefined) - .map(o => o.name); - - // Delete any default that is not undefined. - this._cleanDefaults(defaults, undefinedOptions); - - return defaults; - } -} diff --git a/packages/angular/cli/models/workspace-loader.ts b/packages/angular/cli/models/workspace-loader.ts deleted file mode 100644 index e8785f7dc209..000000000000 --- a/packages/angular/cli/models/workspace-loader.ts +++ /dev/null @@ -1,92 +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, - basename, - dirname, - experimental, - join, - normalize, - virtualFs, -} from '@angular-devkit/core'; -import * as fs from 'fs'; -import { homedir } from 'os'; -import { Observable, of } from 'rxjs'; -import { concatMap, tap } from 'rxjs/operators'; -import { findUp } from '../utilities/find-up'; - - -// TODO: error out instead of returning null when workspace cannot be found. -export class WorkspaceLoader { - private _workspaceCacheMap = new Map(); - // TODO: add remaining fallbacks. - private _configFileNames = [ - normalize('.angular.json'), - normalize('angular.json'), - ]; - constructor(private _host: virtualFs.Host) { } - - loadGlobalWorkspace(): Observable { - return this._getGlobalWorkspaceFilePath().pipe( - concatMap(globalWorkspacePath => this._loadWorkspaceFromPath(globalWorkspacePath)), - ); - } - - loadWorkspace(projectPath?: string): Observable { - return this._getProjectWorkspaceFilePath(projectPath).pipe( - concatMap(globalWorkspacePath => this._loadWorkspaceFromPath(globalWorkspacePath)), - ); - } - - // TODO: do this with the host instead of fs. - private _getProjectWorkspaceFilePath(projectPath?: string): Observable { - // Find the workspace file, either where specified, in the Angular CLI project - // (if it's in node_modules) or from the current process. - const workspaceFilePath = (projectPath && findUp(this._configFileNames, projectPath)) - || findUp(this._configFileNames, process.cwd()) - || findUp(this._configFileNames, __dirname); - - if (workspaceFilePath) { - return of(normalize(workspaceFilePath)); - } else { - throw new Error(`Local workspace file ('angular.json') could not be found.`); - } - } - - // TODO: do this with the host instead of fs. - private _getGlobalWorkspaceFilePath(): Observable { - for (const fileName of this._configFileNames) { - const workspaceFilePath = join(normalize(homedir()), fileName); - - if (fs.existsSync(workspaceFilePath)) { - return of(normalize(workspaceFilePath)); - } - } - - return of(null); - } - - private _loadWorkspaceFromPath(workspacePath: Path | null) { - if (!workspacePath) { - return of(null); - } - - if (this._workspaceCacheMap.has(workspacePath)) { - return of(this._workspaceCacheMap.get(workspacePath) || null); - } - - const workspaceRoot = dirname(workspacePath); - const workspaceFileName = basename(workspacePath); - const workspace = new experimental.workspace.Workspace(workspaceRoot, this._host); - - return workspace.loadWorkspaceFromHost(workspaceFileName).pipe( - tap(workspace => this._workspaceCacheMap.set(workspacePath, workspace)), - ); - } -} diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 9eca5a89afff..abee2e8b2a96 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -1,28 +1,20 @@ { "name": "@angular/cli", - "version": "6.1.0-beta.0", + "version": "0.0.0-PLACEHOLDER", "description": "CLI tool for Angular", "main": "lib/cli/index.js", - "trackingCode": "UA-8594346-19", "bin": { - "ng": "./bin/ng" + "ng": "./bin/ng.js" }, "keywords": [ "angular", "angular-cli", "Angular CLI" ], - "scripts": { - "postinstall": "node ./bin/ng-update-message.js" - }, "repository": { "type": "git", "url": "https://github.com/angular/angular-cli.git" }, - "engines": { - "node": ">= 8.9.0", - "npm": ">= 5.5.1" - }, "author": "Angular Authors", "license": "MIT", "bugs": { @@ -30,18 +22,34 @@ }, "homepage": "https://github.com/angular/angular-cli", "dependencies": { - "@angular-devkit/architect": "0.0.0", - "@angular-devkit/core": "0.0.0", - "@angular-devkit/schematics": "0.0.0", - "@schematics/angular": "0.0.0", - "@schematics/update": "0.0.0", - "opn": "^5.3.0", - "rxjs": "^6.0.0", - "semver": "^5.1.0", - "symbol-observable": "^1.2.0", - "yargs-parser": "^10.0.0" + "@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER", + "@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER", + "@angular-devkit/schematics": "workspace:0.0.0-PLACEHOLDER", + "@inquirer/prompts": "7.5.0", + "@listr2/prompt-adapter-inquirer": "2.0.22", + "@schematics/angular": "workspace:0.0.0-PLACEHOLDER", + "@yarnpkg/lockfile": "1.1.0", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "8.3.3", + "npm-package-arg": "12.0.2", + "npm-pick-manifest": "10.0.0", + "pacote": "20.0.0", + "resolve": "1.22.10", + "semver": "7.7.1", + "yargs": "17.7.2" }, "ng-update": { - "migrations": "@schematics/angular/migrations/migration-collection.json" + "migrations": "@schematics/angular/migrations/migration-collection.json", + "packageGroup": { + "@angular/cli": "0.0.0-PLACEHOLDER", + "@angular/build": "0.0.0-PLACEHOLDER", + "@angular/ssr": "0.0.0-PLACEHOLDER", + "@angular-devkit/architect": "0.0.0-EXPERIMENTAL-PLACEHOLDER", + "@angular-devkit/build-angular": "0.0.0-PLACEHOLDER", + "@angular-devkit/build-webpack": "0.0.0-EXPERIMENTAL-PLACEHOLDER", + "@angular-devkit/core": "0.0.0-PLACEHOLDER", + "@angular-devkit/schematics": "0.0.0-PLACEHOLDER" + } } } diff --git a/packages/angular/cli/plugins/karma.js b/packages/angular/cli/plugins/karma.js deleted file mode 100644 index 821352a15b43..000000000000 --- a/packages/angular/cli/plugins/karma.js +++ /dev/null @@ -1,4 +0,0 @@ -throw new Error( - 'In Angular CLI >6.0 the Karma plugin is now exported by "@angular-devkit/build-angular" instead.\n' - + 'Please replace "@angular/cli" with "@angular-devkit/build-angular" in your "karma.conf.js" file.' -); diff --git a/packages/angular/cli/src/analytics/analytics-collector.ts b/packages/angular/cli/src/analytics/analytics-collector.ts new file mode 100644 index 000000000000..052bc5cbe74c --- /dev/null +++ b/packages/angular/cli/src/analytics/analytics-collector.ts @@ -0,0 +1,207 @@ +/** + * @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 { randomUUID } from 'node:crypto'; +import * as https from 'node:https'; +import * as os from 'node:os'; +import * as querystring from 'node:querystring'; +import * as semver from 'semver'; +import type { CommandContext } from '../command-builder/command-module'; +import { ngDebug } from '../utilities/environment-options'; +import { assertIsError } from '../utilities/error'; +import { VERSION } from '../utilities/version'; +import { + EventCustomDimension, + EventCustomMetric, + PrimitiveTypes, + RequestParameter, + UserCustomDimension, +} from './analytics-parameters'; + +const TRACKING_ID_PROD = 'G-VETNJBW8L4'; +const TRACKING_ID_STAGING = 'G-TBMPRL1BTM'; + +export class AnalyticsCollector { + private trackingEventsQueue: Record[] | undefined; + private readonly requestParameterStringified: string; + private readonly userParameters: Record; + + constructor( + private context: CommandContext, + userId: string, + ) { + const requestParameters: Partial> = { + [RequestParameter.ProtocolVersion]: 2, + [RequestParameter.ClientId]: userId, + [RequestParameter.UserId]: userId, + [RequestParameter.TrackingId]: + /^\d+\.\d+\.\d+$/.test(VERSION.full) && VERSION.full !== '0.0.0' + ? TRACKING_ID_PROD + : TRACKING_ID_STAGING, + + // Built-in user properties + [RequestParameter.SessionId]: randomUUID(), + [RequestParameter.UserAgentArchitecture]: os.arch(), + [RequestParameter.UserAgentPlatform]: os.platform(), + [RequestParameter.UserAgentPlatformVersion]: os.release(), + [RequestParameter.UserAgentMobile]: 0, + [RequestParameter.SessionEngaged]: 1, + // The below is needed for tech details to be collected. + [RequestParameter.UserAgentFullVersionList]: + 'Google%20Chrome;111.0.5563.64|Not(A%3ABrand;8.0.0.0|Chromium;111.0.5563.64', + }; + + if (ngDebug) { + requestParameters[RequestParameter.DebugView] = 1; + } + + this.requestParameterStringified = querystring.stringify(requestParameters); + + const parsedVersion = semver.parse(process.version); + const packageManagerVersion = context.packageManager.version; + + this.userParameters = { + // While architecture is being collect by GA as UserAgentArchitecture. + // It doesn't look like there is a way to query this. Therefore we collect this as a custom user dimension too. + [UserCustomDimension.OsArchitecture]: os.arch(), + // While User ID is being collected by GA, this is not visible in reports/for filtering. + [UserCustomDimension.UserId]: userId, + [UserCustomDimension.NodeVersion]: parsedVersion + ? `${parsedVersion.major}.${parsedVersion.minor}.${parsedVersion.patch}` + : 'other', + [UserCustomDimension.NodeMajorVersion]: parsedVersion?.major, + [UserCustomDimension.PackageManager]: context.packageManager.name, + [UserCustomDimension.PackageManagerVersion]: packageManagerVersion, + [UserCustomDimension.PackageManagerMajorVersion]: packageManagerVersion + ? +packageManagerVersion.split('.', 1)[0] + : undefined, + [UserCustomDimension.AngularCLIVersion]: VERSION.full, + [UserCustomDimension.AngularCLIMajorVersion]: VERSION.major, + }; + } + + reportWorkspaceInfoEvent( + parameters: Partial>, + ): void { + this.event('workspace_info', parameters); + } + + reportRebuildRunEvent( + parameters: Partial< + Record + >, + ): void { + this.event('run_rebuild', parameters); + } + + reportBuildRunEvent( + parameters: Partial< + Record + >, + ): void { + this.event('run_build', parameters); + } + + reportArchitectRunEvent(parameters: Partial>): void { + this.event('run_architect', parameters); + } + + reportSchematicRunEvent(parameters: Partial>): void { + this.event('run_schematic', parameters); + } + + reportCommandRunEvent(command: string): void { + this.event('run_command', { [EventCustomDimension.Command]: command }); + } + + private event(eventName: string, parameters?: Record): void { + this.trackingEventsQueue ??= []; + this.trackingEventsQueue.push({ + ...this.userParameters, + ...parameters, + 'en': eventName, + }); + } + + /** + * Flush on an interval (if the event loop is waiting). + * + * @returns a method that when called will terminate the periodic + * flush and call flush one last time. + */ + periodFlush(): () => Promise { + let analyticsFlushPromise = Promise.resolve(); + const analyticsFlushInterval = setInterval(() => { + if (this.trackingEventsQueue?.length) { + analyticsFlushPromise = analyticsFlushPromise.then(() => this.flush()); + } + }, 4000); + + return () => { + clearInterval(analyticsFlushInterval); + + // Flush one last time. + return analyticsFlushPromise.then(() => this.flush()); + }; + } + + async flush(): Promise { + const pendingTrackingEvents = this.trackingEventsQueue; + this.context.logger.debug(`Analytics flush size. ${pendingTrackingEvents?.length}.`); + + if (!pendingTrackingEvents?.length) { + return; + } + + // The below is needed so that if flush is called multiple times, + // we don't report the same event multiple times. + this.trackingEventsQueue = undefined; + + try { + await this.send(pendingTrackingEvents); + } catch (error) { + // Failure to report analytics shouldn't crash the CLI. + assertIsError(error); + this.context.logger.debug(`Send analytics error. ${error.message}.`); + } + } + + private async send(data: Record[]): Promise { + return new Promise((resolve, reject) => { + const request = https.request( + { + host: 'www.google-analytics.com', + method: 'POST', + path: '/g/collect?' + this.requestParameterStringified, + headers: { + // The below is needed for tech details to be collected even though we provide our own information from the OS Node.js module + 'user-agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36', + }, + }, + (response) => { + // The below is needed as otherwise the response will never close which will cause the CLI not to terminate. + response.on('data', () => {}); + + if (response.statusCode !== 200 && response.statusCode !== 204) { + reject( + new Error(`Analytics reporting failed with status code: ${response.statusCode}.`), + ); + } else { + resolve(); + } + }, + ); + + request.on('error', reject); + const queryParameters = data.map((p) => querystring.stringify(p)).join('\n'); + request.write(queryParameters); + request.end(); + }); + } +} diff --git a/packages/angular/cli/src/analytics/analytics-parameters.mts b/packages/angular/cli/src/analytics/analytics-parameters.mts new file mode 100644 index 000000000000..8a667dd9d2b8 --- /dev/null +++ b/packages/angular/cli/src/analytics/analytics-parameters.mts @@ -0,0 +1,105 @@ +/** + * @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 is a copy of analytics-parameters.ts and is needed for `yarn admin validate-user-analytics` due to ts-node. */ + +/** + * GA built-in request parameters + * @see https://www.thyngster.com/ga4-measurement-protocol-cheatsheet + * @see http://go/depot/google3/analytics/container_tag/templates/common/gold/mpv2_schema.js + */ +export enum RequestParameter { + ClientId = 'cid', + DebugView = '_dbg', + GtmVersion = 'gtm', + Language = 'ul', + NewToSite = '_nsi', + NonInteraction = 'ni', + PageLocation = 'dl', + PageTitle = 'dt', + ProtocolVersion = 'v', + SessionEngaged = 'seg', + SessionId = 'sid', + SessionNumber = 'sct', + SessionStart = '_ss', + TrackingId = 'tid', + TrafficType = 'tt', + UserAgentArchitecture = 'uaa', + UserAgentBitness = 'uab', + UserAgentFullVersionList = 'uafvl', + UserAgentMobile = 'uamb', + UserAgentModel = 'uam', + UserAgentPlatform = 'uap', + UserAgentPlatformVersion = 'uapv', + UserId = 'uid', +} + +/** + * User scoped custom dimensions. + * @remarks + * - User custom dimensions limit is 25. + * - `up.*` string type. + * - `upn.*` number type. + * @see https://support.google.com/analytics/answer/10075209?hl=en + */ +export enum UserCustomDimension { + UserId = 'up.ng_user_id', + OsArchitecture = 'up.ng_os_architecture', + NodeVersion = 'up.ng_node_version', + NodeMajorVersion = 'upn.ng_node_major_version', + AngularCLIVersion = 'up.ng_cli_version', + AngularCLIMajorVersion = 'upn.ng_cli_major_version', + PackageManager = 'up.ng_package_manager', + PackageManagerVersion = 'up.ng_pkg_manager_version', + PackageManagerMajorVersion = 'upn.ng_pkg_manager_major_v', +} + +/** + * Event scoped custom dimensions. + * @remarks + * - Event custom dimensions limit is 50. + * - `ep.*` string type. + * - `epn.*` number type. + * @see https://support.google.com/analytics/answer/10075209?hl=en + */ +export enum EventCustomDimension { + Command = 'ep.ng_command', + SchematicCollectionName = 'ep.ng_schematic_collection_name', + SchematicName = 'ep.ng_schematic_name', + Standalone = 'ep.ng_standalone', + SSR = 'ep.ng_ssr', + Style = 'ep.ng_style', + Routing = 'ep.ng_routing', + InlineTemplate = 'ep.ng_inline_template', + InlineStyle = 'ep.ng_inline_style', + BuilderTarget = 'ep.ng_builder_target', + Aot = 'ep.ng_aot', + Optimization = 'ep.ng_optimization', +} + +/** + * Event scoped custom mertics. + * @remarks + * - Event scoped custom mertics limit is 50. + * - `ep.*` string type. + * - `epn.*` number type. + * @see https://support.google.com/analytics/answer/10075209?hl=en + */ +export enum EventCustomMetric { + AllChunksCount = 'epn.ng_all_chunks_count', + LazyChunksCount = 'epn.ng_lazy_chunks_count', + InitialChunksCount = 'epn.ng_initial_chunks_count', + ChangedChunksCount = 'epn.ng_changed_chunks_count', + DurationInMs = 'epn.ng_duration_ms', + CssSizeInBytes = 'epn.ng_css_size_bytes', + JsSizeInBytes = 'epn.ng_js_size_bytes', + NgComponentCount = 'epn.ng_component_count', + AllProjectsCount = 'epn.all_projects_count', + LibraryProjectsCount = 'epn.libs_projects_count', + ApplicationProjectsCount = 'epn.apps_projects_count', +} diff --git a/packages/angular/cli/src/analytics/analytics-parameters.ts b/packages/angular/cli/src/analytics/analytics-parameters.ts new file mode 100644 index 000000000000..08ee5d72a684 --- /dev/null +++ b/packages/angular/cli/src/analytics/analytics-parameters.ts @@ -0,0 +1,107 @@ +/** + * @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 + */ + +/** Any changes in this file needs to be done in the mts version. */ + +export type PrimitiveTypes = string | number | boolean; + +/** + * GA built-in request parameters + * @see https://www.thyngster.com/ga4-measurement-protocol-cheatsheet + * @see http://go/depot/google3/analytics/container_tag/templates/common/gold/mpv2_schema.js + */ +export enum RequestParameter { + ClientId = 'cid', + DebugView = '_dbg', + GtmVersion = 'gtm', + Language = 'ul', + NewToSite = '_nsi', + NonInteraction = 'ni', + PageLocation = 'dl', + PageTitle = 'dt', + ProtocolVersion = 'v', + SessionEngaged = 'seg', + SessionId = 'sid', + SessionNumber = 'sct', + SessionStart = '_ss', + TrackingId = 'tid', + TrafficType = 'tt', + UserAgentArchitecture = 'uaa', + UserAgentBitness = 'uab', + UserAgentFullVersionList = 'uafvl', + UserAgentMobile = 'uamb', + UserAgentModel = 'uam', + UserAgentPlatform = 'uap', + UserAgentPlatformVersion = 'uapv', + UserId = 'uid', +} + +/** + * User scoped custom dimensions. + * @remarks + * - User custom dimensions limit is 25. + * - `up.*` string type. + * - `upn.*` number type. + * @see https://support.google.com/analytics/answer/10075209?hl=en + */ +export enum UserCustomDimension { + UserId = 'up.ng_user_id', + OsArchitecture = 'up.ng_os_architecture', + NodeVersion = 'up.ng_node_version', + NodeMajorVersion = 'upn.ng_node_major_version', + AngularCLIVersion = 'up.ng_cli_version', + AngularCLIMajorVersion = 'upn.ng_cli_major_version', + PackageManager = 'up.ng_package_manager', + PackageManagerVersion = 'up.ng_pkg_manager_version', + PackageManagerMajorVersion = 'upn.ng_pkg_manager_major_v', +} + +/** + * Event scoped custom dimensions. + * @remarks + * - Event custom dimensions limit is 50. + * - `ep.*` string type. + * - `epn.*` number type. + * @see https://support.google.com/analytics/answer/10075209?hl=en + */ +export enum EventCustomDimension { + Command = 'ep.ng_command', + SchematicCollectionName = 'ep.ng_schematic_collection_name', + SchematicName = 'ep.ng_schematic_name', + Standalone = 'ep.ng_standalone', + SSR = 'ep.ng_ssr', + Style = 'ep.ng_style', + Routing = 'ep.ng_routing', + InlineTemplate = 'ep.ng_inline_template', + InlineStyle = 'ep.ng_inline_style', + BuilderTarget = 'ep.ng_builder_target', + Aot = 'ep.ng_aot', + Optimization = 'ep.ng_optimization', +} + +/** + * Event scoped custom mertics. + * @remarks + * - Event scoped custom mertics limit is 50. + * - `ep.*` string type. + * - `epn.*` number type. + * @see https://support.google.com/analytics/answer/10075209?hl=en + */ +export enum EventCustomMetric { + AllChunksCount = 'epn.ng_all_chunks_count', + LazyChunksCount = 'epn.ng_lazy_chunks_count', + InitialChunksCount = 'epn.ng_initial_chunks_count', + ChangedChunksCount = 'epn.ng_changed_chunks_count', + DurationInMs = 'epn.ng_duration_ms', + CssSizeInBytes = 'epn.ng_css_size_bytes', + JsSizeInBytes = 'epn.ng_js_size_bytes', + NgComponentCount = 'epn.ng_component_count', + AllProjectsCount = 'epn.all_projects_count', + LibraryProjectsCount = 'epn.libs_projects_count', + ApplicationProjectsCount = 'epn.apps_projects_count', +} diff --git a/packages/angular/cli/src/analytics/analytics.ts b/packages/angular/cli/src/analytics/analytics.ts new file mode 100644 index 000000000000..752b0dfca88a --- /dev/null +++ b/packages/angular/cli/src/analytics/analytics.ts @@ -0,0 +1,214 @@ +/** + * @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 { json, tags } from '@angular-devkit/core'; +import { randomUUID } from 'node:crypto'; +import type { CommandContext } from '../command-builder/command-module'; +import { colors } from '../utilities/color'; +import { getWorkspace } from '../utilities/config'; +import { analyticsDisabled } from '../utilities/environment-options'; +import { askConfirmation } from '../utilities/prompt'; +import { isTTY } from '../utilities/tty'; + +/* eslint-disable no-console */ + +/** + * This is the ultimate safelist for checking if a package name is safe to report to analytics. + */ +export const analyticsPackageSafelist = [ + /^@angular\//, + /^@angular-devkit\//, + /^@nguniversal\//, + '@schematics/angular', +]; + +export function isPackageNameSafeForAnalytics(name: string): boolean { + return analyticsPackageSafelist.some((pattern) => { + if (typeof pattern == 'string') { + return pattern === name; + } else { + return pattern.test(name); + } + }); +} + +/** + * Set analytics settings. This does not work if the user is not inside a project. + * @param global Which config to use. "global" for user-level, and "local" for project-level. + * @param value Either a user ID, true to generate a new User ID, or false to disable analytics. + */ +export async function setAnalyticsConfig(global: boolean, value: string | boolean): Promise { + const level = global ? 'global' : 'local'; + const workspace = await getWorkspace(level); + if (!workspace) { + throw new Error(`Could not find ${level} workspace.`); + } + + const cli = (workspace.extensions['cli'] ??= {}); + if (!workspace || !json.isJsonObject(cli)) { + throw new Error(`Invalid config found at ${workspace.filePath}. CLI should be an object.`); + } + + cli.analytics = value === true ? randomUUID() : value; + await workspace.save(); +} + +/** + * Prompt the user for usage gathering permission. + * @param force Whether to ask regardless of whether or not the user is using an interactive shell. + * @return Whether or not the user was shown a prompt. + */ +export async function promptAnalytics( + context: CommandContext, + global: boolean, + force = false, +): Promise { + const level = global ? 'global' : 'local'; + const workspace = await getWorkspace(level); + if (!workspace) { + throw new Error(`Could not find a ${level} workspace. Are you in a project?`); + } + + if (force || isTTY()) { + const answer = await askConfirmation( + ` +Would you like to share pseudonymous usage data about this project with the Angular Team +at Google under Google's Privacy Policy at https://policies.google.com/privacy. For more +details and how to change this setting, see https://angular.dev/cli/analytics. + + `, + false, + ); + + await setAnalyticsConfig(global, answer); + + if (answer) { + console.log(''); + console.log( + tags.stripIndent` + Thank you for sharing pseudonymous usage data. Should you change your mind, the following + command will disable this feature entirely: + + ${colors.yellow(`ng analytics disable${global ? ' --global' : ''}`)} + `, + ); + console.log(''); + } + + process.stderr.write(await getAnalyticsInfoString(context)); + + return true; + } + + return false; +} + +/** + * Get the analytics user id. + * + * @returns + * - `string` user id. + * - `false` when disabled. + * - `undefined` when not configured. + */ +async function getAnalyticsUserIdForLevel( + level: 'local' | 'global', +): Promise { + if (analyticsDisabled) { + return false; + } + + const workspace = await getWorkspace(level); + const analyticsConfig: string | undefined | null | { uid?: string } | boolean = + workspace?.getCli()?.['analytics']; + + if (analyticsConfig === false) { + return false; + } else if (analyticsConfig === undefined || analyticsConfig === null) { + return undefined; + } else { + if (typeof analyticsConfig == 'string') { + return analyticsConfig; + } else if (typeof analyticsConfig == 'object' && typeof analyticsConfig['uid'] == 'string') { + return analyticsConfig['uid']; + } + + return undefined; + } +} + +export async function getAnalyticsUserId( + context: CommandContext, + skipPrompt = false, +): Promise { + const { workspace } = context; + // Global config takes precedence over local config only for the disabled check. + // IE: + // global: disabled & local: enabled = disabled + // global: id: 123 & local: id: 456 = 456 + + // check global + const globalConfig = await getAnalyticsUserIdForLevel('global'); + if (globalConfig === false) { + return undefined; + } + + // Not disabled globally, check locally or not set globally and command is run outside of workspace example: `ng new` + if (workspace || globalConfig === undefined) { + const level = workspace ? 'local' : 'global'; + let localOrGlobalConfig = await getAnalyticsUserIdForLevel(level); + if (localOrGlobalConfig === undefined) { + if (!skipPrompt) { + // config is unset, prompt user. + // TODO: This should honor the `no-interactive` option. + // It is currently not an `ng` option but rather only an option for specific commands. + // The concept of `ng`-wide options are needed to cleanly handle this. + await promptAnalytics(context, !workspace /** global */); + localOrGlobalConfig = await getAnalyticsUserIdForLevel(level); + } + } + + if (localOrGlobalConfig === false) { + return undefined; + } else if (typeof localOrGlobalConfig === 'string') { + return localOrGlobalConfig; + } + } + + return globalConfig; +} + +function analyticsConfigValueToHumanFormat(value: unknown): 'enabled' | 'disabled' | 'not set' { + if (value === false) { + return 'disabled'; + } else if (typeof value === 'string' || value === true) { + return 'enabled'; + } else { + return 'not set'; + } +} + +export async function getAnalyticsInfoString(context: CommandContext): Promise { + const analyticsInstance = await getAnalyticsUserId(context, true /** skipPrompt */); + + const { globalConfiguration, workspace: localWorkspace } = context; + const globalSetting = globalConfiguration?.getCli()?.['analytics']; + const localSetting = localWorkspace?.getCli()?.['analytics']; + + return ( + tags.stripIndents` + Global setting: ${analyticsConfigValueToHumanFormat(globalSetting)} + Local setting: ${ + localWorkspace + ? analyticsConfigValueToHumanFormat(localSetting) + : 'No local workspace configuration file.' + } + Effective status: ${analyticsInstance ? 'enabled' : 'disabled'} + ` + '\n' + ); +} diff --git a/packages/angular/cli/src/command-builder/architect-base-command-module.ts b/packages/angular/cli/src/command-builder/architect-base-command-module.ts new file mode 100644 index 000000000000..566e0e62b209 --- /dev/null +++ b/packages/angular/cli/src/command-builder/architect-base-command-module.ts @@ -0,0 +1,300 @@ +/** + * @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 { Architect, Target } from '@angular-devkit/architect'; +import { + NodeModulesBuilderInfo, + WorkspaceNodeModulesArchitectHost, +} from '@angular-devkit/architect/node'; +import { json } from '@angular-devkit/core'; +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { isPackageNameSafeForAnalytics } from '../analytics/analytics'; +import { EventCustomDimension, EventCustomMetric } from '../analytics/analytics-parameters'; +import { assertIsError } from '../utilities/error'; +import { askConfirmation, askQuestion } from '../utilities/prompt'; +import { isTTY } from '../utilities/tty'; +import { + CommandModule, + CommandModuleError, + CommandModuleImplementation, + CommandScope, + OtherOptions, +} from './command-module'; +import { Option, parseJsonSchemaToOptions } from './utilities/json-schema'; + +export interface MissingTargetChoice { + name: string; + value: string; +} + +export abstract class ArchitectBaseCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + override scope = CommandScope.In; + protected readonly missingTargetChoices: MissingTargetChoice[] | undefined; + + protected async runSingleTarget(target: Target, options: OtherOptions): Promise { + const architectHost = this.getArchitectHost(); + + let builderName: string; + try { + builderName = await architectHost.getBuilderNameForTarget(target); + } catch (e) { + assertIsError(e); + + return this.onMissingTarget(e.message); + } + + const isAngularBuild = builderName.startsWith('@angular/build:'); + + const { logger } = this.context; + const run = await this.getArchitect(isAngularBuild).scheduleTarget( + target, + options as json.JsonObject, + { + logger, + }, + ); + + const analytics = isPackageNameSafeForAnalytics(builderName) + ? await this.getAnalytics() + : undefined; + + let outputSubscription; + if (analytics) { + analytics.reportArchitectRunEvent({ + [EventCustomDimension.BuilderTarget]: builderName, + }); + + let firstRun = true; + outputSubscription = run.output.subscribe(({ stats }) => { + const parameters = this.builderStatsToAnalyticsParameters(stats, builderName); + if (!parameters) { + return; + } + + if (firstRun) { + firstRun = false; + analytics.reportBuildRunEvent(parameters); + } else { + analytics.reportRebuildRunEvent(parameters); + } + }); + } + + try { + const { error, success } = await run.lastOutput; + if (error) { + logger.error(error); + } + + return success ? 0 : 1; + } finally { + await run.stop(); + outputSubscription?.unsubscribe(); + } + } + + private builderStatsToAnalyticsParameters( + stats: json.JsonValue, + builderName: string, + ): Partial< + | Record + | undefined + > { + if (!stats || typeof stats !== 'object' || !('durationInMs' in stats)) { + return undefined; + } + + const { + optimization, + allChunksCount, + aot, + lazyChunksCount, + initialChunksCount, + durationInMs, + changedChunksCount, + cssSizeInBytes, + jsSizeInBytes, + ngComponentCount, + } = stats; + + return { + [EventCustomDimension.BuilderTarget]: builderName, + [EventCustomDimension.Aot]: aot, + [EventCustomDimension.Optimization]: optimization, + [EventCustomMetric.AllChunksCount]: allChunksCount, + [EventCustomMetric.LazyChunksCount]: lazyChunksCount, + [EventCustomMetric.InitialChunksCount]: initialChunksCount, + [EventCustomMetric.ChangedChunksCount]: changedChunksCount, + [EventCustomMetric.DurationInMs]: durationInMs, + [EventCustomMetric.JsSizeInBytes]: jsSizeInBytes, + [EventCustomMetric.CssSizeInBytes]: cssSizeInBytes, + [EventCustomMetric.NgComponentCount]: ngComponentCount, + }; + } + + private _architectHost: WorkspaceNodeModulesArchitectHost | undefined; + protected getArchitectHost(): WorkspaceNodeModulesArchitectHost { + if (this._architectHost) { + return this._architectHost; + } + + const workspace = this.getWorkspaceOrThrow(); + + return (this._architectHost = new WorkspaceNodeModulesArchitectHost( + workspace, + workspace.basePath, + )); + } + + private _architect: Architect | undefined; + protected getArchitect(skipUndefinedArrayTransform: boolean): Architect { + if (this._architect) { + return this._architect; + } + + const registry = new json.schema.CoreSchemaRegistry(); + if (skipUndefinedArrayTransform) { + registry.addPostTransform(json.schema.transforms.addUndefinedObjectDefaults); + } else { + registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); + } + registry.useXDeprecatedProvider((msg) => this.context.logger.warn(msg)); + + const architectHost = this.getArchitectHost(); + + return (this._architect = new Architect(architectHost, registry)); + } + + protected async getArchitectTargetOptions(target: Target): Promise { + const architectHost = this.getArchitectHost(); + let builderConf: string; + + try { + builderConf = await architectHost.getBuilderNameForTarget(target); + } catch { + return []; + } + + let builderDesc: NodeModulesBuilderInfo; + try { + builderDesc = await architectHost.resolveBuilder(builderConf); + } catch (e) { + assertIsError(e); + if (e.code === 'MODULE_NOT_FOUND') { + this.warnOnMissingNodeModules(); + throw new CommandModuleError(`Could not find the '${builderConf}' builder's node package.`); + } + + throw e; + } + + return parseJsonSchemaToOptions( + new json.schema.CoreSchemaRegistry(), + builderDesc.optionSchema as json.JsonObject, + true, + ); + } + + private warnOnMissingNodeModules(): void { + const basePath = this.context.workspace?.basePath; + if (!basePath) { + return; + } + + // Check if yarn PnP is used. https://yarnpkg.com/advanced/pnpapi#processversionspnp + if (process.versions.pnp) { + return; + } + + // Check for a `node_modules` directory (npm, yarn non-PnP, etc.) + if (existsSync(resolve(basePath, 'node_modules'))) { + return; + } + + this.context.logger.warn( + `Node packages may not be installed. Try installing with '${this.context.packageManager.name} install'.`, + ); + } + + protected getArchitectTarget(): string { + return this.commandName; + } + + protected async onMissingTarget(defaultMessage: string): Promise<1> { + const { logger } = this.context; + const choices = this.missingTargetChoices; + + if (!choices?.length) { + logger.error(defaultMessage); + + return 1; + } + + const missingTargetMessage = + `Cannot find "${this.getArchitectTarget()}" target for the specified project.\n` + + `You can add a package that implements these capabilities.\n\n` + + `For example:\n` + + choices.map(({ name, value }) => ` ${name}: ng add ${value}`).join('\n') + + '\n'; + + if (isTTY()) { + // Use prompts to ask the user if they'd like to install a package. + logger.warn(missingTargetMessage); + + const packageToInstall = await this.getMissingTargetPackageToInstall(choices); + if (packageToInstall) { + // Example run: `ng add angular-eslint`. + const AddCommandModule = (await import('../commands/add/cli')).default; + await new AddCommandModule(this.context).run({ + interactive: true, + force: false, + dryRun: false, + defaults: false, + collection: packageToInstall, + }); + } + } else { + // Non TTY display error message. + logger.error(missingTargetMessage); + } + + return 1; + } + + private async getMissingTargetPackageToInstall( + choices: MissingTargetChoice[], + ): Promise { + if (choices.length === 1) { + // Single choice + const { name, value } = choices[0]; + if (await askConfirmation(`Would you like to add ${name} now?`, true, false)) { + return value; + } + + return null; + } + + // Multiple choice + return askQuestion( + `Would you like to add a package with "${this.getArchitectTarget()}" capabilities now?`, + [ + { + name: 'No', + value: null, + }, + ...choices, + ], + 0, + null, + ); + } +} diff --git a/packages/angular/cli/src/command-builder/architect-command-module.ts b/packages/angular/cli/src/command-builder/architect-command-module.ts new file mode 100644 index 000000000000..4855b629b360 --- /dev/null +++ b/packages/angular/cli/src/command-builder/architect-command-module.ts @@ -0,0 +1,208 @@ +/** + * @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 { Target } from '@angular-devkit/architect'; +import { workspaces } from '@angular-devkit/core'; +import { Argv } from 'yargs'; +import { getProjectByCwd } from '../utilities/config'; +import { memoize } from '../utilities/memoize'; +import { ArchitectBaseCommandModule } from './architect-base-command-module'; +import { + CommandModuleError, + CommandModuleImplementation, + Options, + OtherOptions, +} from './command-module'; + +export interface ArchitectCommandArgs { + configuration?: string; + project?: string; +} + +export abstract class ArchitectCommandModule + extends ArchitectBaseCommandModule + implements CommandModuleImplementation +{ + abstract readonly multiTarget: boolean; + + findDefaultBuilderName?( + project: workspaces.ProjectDefinition, + target: Target, + ): Promise; + + async builder(argv: Argv): Promise> { + const target = this.getArchitectTarget(); + + // Add default builder if target is not in project and a command default is provided + if (this.findDefaultBuilderName && this.context.workspace) { + for (const [project, projectDefinition] of this.context.workspace.projects) { + if (projectDefinition.targets.has(target)) { + continue; + } + + const defaultBuilder = await this.findDefaultBuilderName(projectDefinition, { + project, + target, + }); + if (defaultBuilder) { + projectDefinition.targets.set(target, { + builder: defaultBuilder, + }); + } + } + } + + const project = this.getArchitectProject(); + const { jsonHelp, getYargsCompletions, help } = this.context.args.options; + + const localYargs: Argv = argv + .positional('project', { + describe: 'The name of the project to build. Can be an application or a library.', + type: 'string', + // Hide choices from JSON help so that we don't display them in AIO. + choices: jsonHelp ? undefined : this.getProjectChoices(), + }) + .option('configuration', { + describe: + `One or more named builder configurations as a comma-separated ` + + `list as specified in the "configurations" section in angular.json.\n` + + `The builder uses the named configurations to run the given target.\n` + + `For more information, see https://angular.dev/reference/configs/workspace-config#alternate-build-configurations.`, + alias: 'c', + type: 'string', + // Show only in when using --help and auto completion because otherwise comma seperated configuration values will be invalid. + // Also, hide choices from JSON help so that we don't display them in AIO. + choices: + (getYargsCompletions || help) && !jsonHelp && project + ? this.getConfigurationChoices(project) + : undefined, + }) + .strict(); + + if (!project) { + return localYargs; + } + + const schemaOptions = await this.getArchitectTargetOptions({ + project, + target, + }); + + return this.addSchemaOptionsToCommand(localYargs, schemaOptions); + } + + async run(options: Options & OtherOptions): Promise { + const target = this.getArchitectTarget(); + + const { configuration = '', project, ...architectOptions } = options; + + if (!project) { + // This runs each target sequentially. + // Running them in parallel would jumble the log messages. + let result = 0; + const projectNames = this.getProjectNamesByTarget(target); + if (!projectNames) { + return this.onMissingTarget('Cannot determine project or target for command.'); + } + + for (const project of projectNames) { + result |= await this.runSingleTarget({ configuration, target, project }, architectOptions); + } + + return result; + } else { + return await this.runSingleTarget({ configuration, target, project }, architectOptions); + } + } + + private getArchitectProject(): string | undefined { + const { options, positional } = this.context.args; + const [, projectName] = positional; + + if (projectName) { + return projectName; + } + + // Yargs allows positional args to be used as flags. + if (typeof options['project'] === 'string') { + return options['project']; + } + + const target = this.getArchitectTarget(); + const projectFromTarget = this.getProjectNamesByTarget(target); + + return projectFromTarget?.length ? projectFromTarget[0] : undefined; + } + + @memoize + private getProjectNamesByTarget(target: string): string[] | undefined { + const workspace = this.getWorkspaceOrThrow(); + const allProjectsForTargetName: string[] = []; + + for (const [name, project] of workspace.projects) { + if (project.targets.has(target)) { + allProjectsForTargetName.push(name); + } + } + + if (allProjectsForTargetName.length === 0) { + return undefined; + } + + if (this.multiTarget) { + // For multi target commands, we always list all projects that have the target. + return allProjectsForTargetName; + } else { + if (allProjectsForTargetName.length === 1) { + return allProjectsForTargetName; + } + + const maybeProject = getProjectByCwd(workspace); + if (maybeProject) { + return allProjectsForTargetName.includes(maybeProject) ? [maybeProject] : undefined; + } + + const { getYargsCompletions, help } = this.context.args.options; + if (!getYargsCompletions && !help) { + // Only issue the below error when not in help / completion mode. + throw new CommandModuleError( + 'Cannot determine project for command.\n' + + 'This is a multi-project workspace and more than one project supports this command. ' + + `Run "ng ${this.command}" to execute the command for a specific project or change the current ` + + 'working directory to a project directory.\n\n' + + `Available projects are:\n${allProjectsForTargetName + .sort() + .map((p) => `- ${p}`) + .join('\n')}`, + ); + } + } + + return undefined; + } + + /** @returns a sorted list of project names to be used for auto completion. */ + private getProjectChoices(): string[] | undefined { + const { workspace } = this.context; + + return workspace ? [...workspace.projects.keys()].sort() : undefined; + } + + /** @returns a sorted list of configuration names to be used for auto completion. */ + private getConfigurationChoices(project: string): string[] | undefined { + const projectDefinition = this.context.workspace?.projects.get(project); + if (!projectDefinition) { + return undefined; + } + + const target = this.getArchitectTarget(); + const configurations = projectDefinition.targets.get(target)?.configurations; + + return configurations ? Object.keys(configurations).sort() : undefined; + } +} diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts new file mode 100644 index 000000000000..a97815eec027 --- /dev/null +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -0,0 +1,297 @@ +/** + * @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, schema } from '@angular-devkit/core'; +import { readFileSync } from 'node:fs'; +import * as path from 'node:path'; +import yargs, { + ArgumentsCamelCase, + Argv, + CamelCaseKey, + CommandModule as YargsCommandModule, +} from 'yargs'; +import { Parser as yargsParser } from 'yargs/helpers'; +import { getAnalyticsUserId } from '../analytics/analytics'; +import { AnalyticsCollector } from '../analytics/analytics-collector'; +import { EventCustomDimension, EventCustomMetric } from '../analytics/analytics-parameters'; +import { considerSettingUpAutocompletion } from '../utilities/completion'; +import { AngularWorkspace } from '../utilities/config'; +import { memoize } from '../utilities/memoize'; +import { PackageManagerUtils } from '../utilities/package-manager'; +import { Option, addSchemaOptionsToCommand } from './utilities/json-schema'; + +export type Options = { [key in keyof T as CamelCaseKey]: T[key] }; + +export enum CommandScope { + /** Command can only run inside an Angular workspace. */ + In, + + /** Command can only run outside an Angular workspace. */ + Out, + + /** Command can run inside and outside an Angular workspace. */ + Both, +} + +export interface CommandContext { + currentDirectory: string; + root: string; + workspace?: AngularWorkspace; + globalConfiguration: AngularWorkspace; + logger: logging.Logger; + packageManager: PackageManagerUtils; + + /** Arguments parsed in free-from without parser configuration. */ + args: { + positional: string[]; + options: { + help: boolean; + jsonHelp: boolean; + getYargsCompletions: boolean; + } & Record; + }; +} + +export type OtherOptions = Record; + +export interface CommandModuleImplementation + extends Omit, 'builder' | 'handler'> { + /** Scope in which the command can be executed in. */ + scope: CommandScope; + + /** Path used to load the long description for the command in JSON help text. */ + longDescriptionPath?: string; + + /** Object declaring the options the command accepts, or a function accepting and returning a yargs instance. */ + builder(argv: Argv): Promise> | Argv; + + /** A function which will be passed the parsed argv. */ + run(options: Options & OtherOptions): Promise | number | void; +} + +export interface FullDescribe { + describe?: string; + longDescription?: string; + longDescriptionRelativePath?: string; +} + +export abstract class CommandModule implements CommandModuleImplementation { + abstract readonly command: string; + abstract readonly describe: string | false; + abstract readonly longDescriptionPath?: string; + protected readonly shouldReportAnalytics: boolean = true; + readonly scope: CommandScope = CommandScope.Both; + + private readonly optionsWithAnalytics = new Map(); + + constructor(protected readonly context: CommandContext) {} + + /** + * Description object which contains the long command descroption. + * This is used to generate JSON help wich is used in AIO. + * + * `false` will result in a hidden command. + */ + public get fullDescribe(): FullDescribe | false { + return this.describe === false + ? false + : { + describe: this.describe, + ...(this.longDescriptionPath + ? { + longDescriptionRelativePath: path + .relative(path.join(__dirname, '../../../../'), this.longDescriptionPath) + .replace(/\\/g, path.posix.sep), + longDescription: readFileSync(this.longDescriptionPath, 'utf8').replace( + /\r\n/g, + '\n', + ), + } + : {}), + }; + } + + protected get commandName(): string { + return this.command.split(' ', 1)[0]; + } + + abstract builder(argv: Argv): Promise> | Argv; + abstract run(options: Options & OtherOptions): Promise | number | void; + + async handler(args: ArgumentsCamelCase & OtherOptions): Promise { + const { _, $0, ...options } = args; + + // Camelize options as yargs will return the object in kebab-case when camel casing is disabled. + const camelCasedOptions: Record = {}; + for (const [key, value] of Object.entries(options)) { + camelCasedOptions[yargsParser.camelCase(key)] = value; + } + + // Set up autocompletion if appropriate. + const autocompletionExitCode = await considerSettingUpAutocompletion( + this.commandName, + this.context.logger, + ); + if (autocompletionExitCode !== undefined) { + process.exitCode = autocompletionExitCode; + + return; + } + + // Gather and report analytics. + const analytics = await this.getAnalytics(); + const stopPeriodicFlushes = analytics && analytics.periodFlush(); + + let exitCode: number | void | undefined; + try { + if (analytics) { + this.reportCommandRunAnalytics(analytics); + this.reportWorkspaceInfoAnalytics(analytics); + } + + exitCode = await this.run(camelCasedOptions as Options & OtherOptions); + } catch (e) { + if (e instanceof schema.SchemaValidationException) { + this.context.logger.fatal(`Error: ${e.message}`); + exitCode = 1; + } else { + throw e; + } + } finally { + await stopPeriodicFlushes?.(); + + if (typeof exitCode === 'number' && exitCode > 0) { + process.exitCode = exitCode; + } + } + } + + @memoize + protected async getAnalytics(): Promise { + if (!this.shouldReportAnalytics) { + return undefined; + } + + const userId = await getAnalyticsUserId( + this.context, + // Don't prompt on `ng update`, 'ng version' or `ng analytics`. + ['version', 'update', 'analytics'].includes(this.commandName), + ); + + return userId ? new AnalyticsCollector(this.context, userId) : undefined; + } + + /** + * Adds schema options to a command also this keeps track of options that are required for analytics. + * **Note:** This method should be called from the command bundler method. + */ + protected addSchemaOptionsToCommand(localYargs: Argv, options: Option[]): Argv { + const optionsWithAnalytics = addSchemaOptionsToCommand( + localYargs, + options, + // This should only be done when `--help` is used otherwise default will override options set in angular.json. + /* includeDefaultValues= */ this.context.args.options.help, + ); + + // Record option of analytics. + for (const [name, userAnalytics] of optionsWithAnalytics) { + this.optionsWithAnalytics.set(name, userAnalytics); + } + + return localYargs; + } + + protected getWorkspaceOrThrow(): AngularWorkspace { + const { workspace } = this.context; + if (!workspace) { + throw new CommandModuleError('A workspace is required for this command.'); + } + + return workspace; + } + + /** + * Flush on an interval (if the event loop is waiting). + * + * @returns a method that when called will terminate the periodic + * flush and call flush one last time. + */ + protected getAnalyticsParameters( + options: (Options & OtherOptions) | OtherOptions, + ): Partial> { + const parameters: Partial< + Record + > = {}; + + const validEventCustomDimensionAndMetrics = new Set([ + ...Object.values(EventCustomDimension), + ...Object.values(EventCustomMetric), + ]); + + for (const [name, ua] of this.optionsWithAnalytics) { + const value = options[name]; + if ( + (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') && + validEventCustomDimensionAndMetrics.has(ua as EventCustomDimension | EventCustomMetric) + ) { + parameters[ua as EventCustomDimension | EventCustomMetric] = value; + } + } + + return parameters; + } + + private reportCommandRunAnalytics(analytics: AnalyticsCollector): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const internalMethods = (yargs as any).getInternalMethods(); + // $0 generate component [name] -> generate_component + // $0 add -> add + const fullCommand = (internalMethods.getUsageInstance().getUsage()[0][0] as string) + .split(' ') + .filter((x) => { + const code = x.charCodeAt(0); + + return code >= 97 && code <= 122; + }) + .join('_'); + + analytics.reportCommandRunEvent(fullCommand); + } + + private reportWorkspaceInfoAnalytics(analytics: AnalyticsCollector): void { + const { workspace } = this.context; + if (!workspace) { + return; + } + + let applicationProjectsCount = 0; + let librariesProjectsCount = 0; + for (const project of workspace.projects.values()) { + switch (project.extensions['projectType']) { + case 'application': + applicationProjectsCount++; + break; + case 'library': + librariesProjectsCount++; + break; + } + } + + analytics.reportWorkspaceInfoEvent({ + [EventCustomMetric.AllProjectsCount]: librariesProjectsCount + applicationProjectsCount, + [EventCustomMetric.ApplicationProjectsCount]: applicationProjectsCount, + [EventCustomMetric.LibraryProjectsCount]: librariesProjectsCount, + }); + } +} + +/** + * Creates an known command module error. + * This is used so during executation we can filter between known validation error and real non handled errors. + */ +export class CommandModuleError extends Error {} diff --git a/packages/angular/cli/src/command-builder/command-runner.ts b/packages/angular/cli/src/command-builder/command-runner.ts new file mode 100644 index 000000000000..2ad2c07b1eeb --- /dev/null +++ b/packages/angular/cli/src/command-builder/command-runner.ts @@ -0,0 +1,165 @@ +/** + * @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 yargs from 'yargs'; +import { Parser } from 'yargs/helpers'; +import { + CommandConfig, + CommandNames, + RootCommands, + RootCommandsAliases, +} from '../commands/command-config'; +import { colors } from '../utilities/color'; +import { AngularWorkspace, getWorkspace } from '../utilities/config'; +import { assertIsError } from '../utilities/error'; +import { PackageManagerUtils } from '../utilities/package-manager'; +import { VERSION } from '../utilities/version'; +import { CommandContext, CommandModuleError } from './command-module'; +import { + CommandModuleConstructor, + addCommandModuleToYargs, + demandCommandFailureMessage, +} from './utilities/command'; +import { jsonHelpUsage } from './utilities/json-help'; +import { normalizeOptionsMiddleware } from './utilities/normalize-options-middleware'; + +const yargsParser = Parser as unknown as typeof Parser.default; + +export async function runCommand(args: string[], logger: logging.Logger): Promise { + const { + $0, + _, + help = false, + jsonHelp = false, + getYargsCompletions = false, + ...rest + } = yargsParser(args, { + boolean: ['help', 'json-help', 'get-yargs-completions'], + alias: { 'collection': 'c' }, + }); + + // When `getYargsCompletions` is true the scriptName 'ng' at index 0 is not removed. + const positional = getYargsCompletions ? _.slice(1) : _; + + let workspace: AngularWorkspace | undefined; + let globalConfiguration: AngularWorkspace; + try { + [workspace, globalConfiguration] = await Promise.all([ + getWorkspace('local'), + getWorkspace('global'), + ]); + } catch (e) { + assertIsError(e); + logger.fatal(e.message); + + return 1; + } + + const root = workspace?.basePath ?? process.cwd(); + const context: CommandContext = { + globalConfiguration, + workspace, + logger, + currentDirectory: process.cwd(), + root, + packageManager: new PackageManagerUtils({ globalConfiguration, workspace, root }), + args: { + positional: positional.map((v) => v.toString()), + options: { + help, + jsonHelp, + getYargsCompletions, + ...rest, + }, + }, + }; + + let localYargs = yargs(args); + for (const CommandModule of await getCommandsToRegister(positional[0])) { + localYargs = addCommandModuleToYargs(localYargs, CommandModule, context); + } + + if (jsonHelp) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const usageInstance = (localYargs as any).getInternalMethods().getUsageInstance(); + usageInstance.help = () => jsonHelpUsage(); + } + + // Add default command to support version option when no subcommand is specified + localYargs.command('*', false, (builder) => + builder.version('version', 'Show Angular CLI version.', VERSION.full), + ); + + await localYargs + .scriptName('ng') + // https://github.com/yargs/yargs/blob/main/docs/advanced.md#customizing-yargs-parser + .parserConfiguration({ + 'populate--': true, + 'unknown-options-as-args': false, + 'dot-notation': false, + 'boolean-negation': true, + 'strip-aliased': true, + 'strip-dashed': true, + 'camel-case-expansion': false, + }) + .option('json-help', { + describe: 'Show help in JSON format.', + implies: ['help'], + hidden: true, + type: 'boolean', + }) + .help('help', 'Shows a help message for this command in the console.') + // A complete list of strings can be found: https://github.com/yargs/yargs/blob/main/locales/en.json + .updateStrings({ + 'Commands:': colors.cyan('Commands:'), + 'Options:': colors.cyan('Options:'), + 'Positionals:': colors.cyan('Arguments:'), + 'deprecated': colors.yellow('deprecated'), + 'deprecated: %s': colors.yellow('deprecated:') + ' %s', + 'Did you mean %s?': 'Unknown command. Did you mean %s?', + }) + .epilogue('For more information, see https://angular.dev/cli/.\n') + .demandCommand(1, demandCommandFailureMessage) + .recommendCommands() + .middleware(normalizeOptionsMiddleware) + .version(false) + .showHelpOnFail(false) + .strict() + .fail((msg, err) => { + throw msg + ? // Validation failed example: `Unknown argument:` + new CommandModuleError(msg) + : // Unknown exception, re-throw. + err; + }) + .wrap(yargs.terminalWidth()) + .parseAsync(); + + return +(process.exitCode ?? 0); +} + +/** + * Get the commands that need to be registered. + * @returns One or more command factories that needs to be registered. + */ +async function getCommandsToRegister( + commandName: string | number, +): Promise { + const commands: CommandConfig[] = []; + if (commandName in RootCommands) { + commands.push(RootCommands[commandName as CommandNames]); + } else if (commandName in RootCommandsAliases) { + commands.push(RootCommandsAliases[commandName]); + } else { + // Unknown command, register every possible command. + Object.values(RootCommands).forEach((c) => commands.push(c)); + } + + return Promise.all(commands.map((command) => command.factory().then((m) => m.default))); +} diff --git a/packages/angular/cli/src/command-builder/schematics-command-module.ts b/packages/angular/cli/src/command-builder/schematics-command-module.ts new file mode 100644 index 000000000000..738fd497382b --- /dev/null +++ b/packages/angular/cli/src/command-builder/schematics-command-module.ts @@ -0,0 +1,404 @@ +/** + * @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 { JsonValue, normalize as devkitNormalize, schema } from '@angular-devkit/core'; +import { Collection, UnsuccessfulWorkflowExecution, formats } from '@angular-devkit/schematics'; +import { + FileSystemCollectionDescription, + FileSystemSchematicDescription, + NodeWorkflow, +} from '@angular-devkit/schematics/tools'; +import { relative } from 'node:path'; +import { Argv } from 'yargs'; +import { isPackageNameSafeForAnalytics } from '../analytics/analytics'; +import { EventCustomDimension } from '../analytics/analytics-parameters'; +import { getProjectByCwd, getSchematicDefaults } from '../utilities/config'; +import { assertIsError } from '../utilities/error'; +import { memoize } from '../utilities/memoize'; +import { isTTY } from '../utilities/tty'; +import { + CommandModule, + CommandModuleImplementation, + CommandScope, + Options, + OtherOptions, +} from './command-module'; +import { Option, parseJsonSchemaToOptions } from './utilities/json-schema'; +import { SchematicEngineHost } from './utilities/schematic-engine-host'; +import { subscribeToWorkflow } from './utilities/schematic-workflow'; + +export const DEFAULT_SCHEMATICS_COLLECTION = '@schematics/angular'; + +export interface SchematicsCommandArgs { + interactive: boolean; + force: boolean; + 'dry-run': boolean; + defaults: boolean; +} + +export interface SchematicsExecutionOptions extends Options { + packageRegistry?: string; +} + +export abstract class SchematicsCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + override scope = CommandScope.In; + protected readonly allowPrivateSchematics: boolean = false; + + async builder(argv: Argv): Promise> { + return argv + .option('interactive', { + describe: 'Enable interactive input prompts.', + type: 'boolean', + default: true, + }) + .option('dry-run', { + describe: 'Run through and reports activity without writing out results.', + type: 'boolean', + alias: ['d'], + default: false, + }) + .option('defaults', { + describe: 'Disable interactive input prompts for options with a default.', + type: 'boolean', + default: false, + }) + .option('force', { + describe: 'Force overwriting of existing files.', + type: 'boolean', + default: false, + }) + .strict(); + } + + /** Get schematic schema options.*/ + protected async getSchematicOptions( + collection: Collection, + schematicName: string, + workflow: NodeWorkflow, + ): Promise { + const schematic = collection.createSchematic(schematicName, true); + const { schemaJson } = schematic.description; + + if (!schemaJson) { + return []; + } + + return parseJsonSchemaToOptions(workflow.registry, schemaJson); + } + + @memoize + protected getOrCreateWorkflowForBuilder(collectionName: string): NodeWorkflow { + return new NodeWorkflow(this.context.root, { + resolvePaths: this.getResolvePaths(collectionName), + engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), + }); + } + + @memoize + protected async getOrCreateWorkflowForExecution( + collectionName: string, + options: SchematicsExecutionOptions, + ): Promise { + const { logger, root, packageManager } = this.context; + const { force, dryRun, packageRegistry } = options; + + const workflow = new NodeWorkflow(root, { + force, + dryRun, + packageManager: packageManager.name, + // A schema registry is required to allow customizing addUndefinedDefaults + registry: new schema.CoreSchemaRegistry(formats.standardFormats), + packageRegistry, + resolvePaths: this.getResolvePaths(collectionName), + schemaValidation: true, + optionTransforms: [ + // Add configuration file defaults + async (schematic, current) => { + const projectName = + typeof current?.project === 'string' ? current.project : this.getProjectName(); + + return { + ...(await getSchematicDefaults(schematic.collection.name, schematic.name, projectName)), + ...current, + }; + }, + ], + engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), + }); + + workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults); + workflow.registry.useXDeprecatedProvider((msg) => logger.warn(msg)); + workflow.registry.addSmartDefaultProvider('projectName', () => this.getProjectName()); + + const workingDir = devkitNormalize(relative(this.context.root, process.cwd())); + workflow.registry.addSmartDefaultProvider('workingDirectory', () => + workingDir === '' ? undefined : workingDir, + ); + + workflow.engineHost.registerOptionsTransform(async (schematic, options) => { + const { + collection: { name: collectionName }, + name: schematicName, + } = schematic; + + const analytics = isPackageNameSafeForAnalytics(collectionName) + ? await this.getAnalytics() + : undefined; + + analytics?.reportSchematicRunEvent({ + [EventCustomDimension.SchematicCollectionName]: collectionName, + [EventCustomDimension.SchematicName]: schematicName, + ...this.getAnalyticsParameters(options as unknown as {}), + }); + + return options; + }); + + if (options.interactive !== false && isTTY()) { + workflow.registry.usePromptProvider(async (definitions: Array) => { + let prompts: typeof import('@inquirer/prompts') | undefined; + const answers: Record = {}; + + for (const definition of definitions) { + if (options.defaults && definition.default !== undefined) { + continue; + } + + // Only load prompt package if needed + prompts ??= await import('@inquirer/prompts'); + + switch (definition.type) { + case 'confirmation': + answers[definition.id] = await prompts.confirm({ + message: definition.message, + default: definition.default as boolean | undefined, + }); + break; + case 'list': + if (!definition.items?.length) { + continue; + } + + answers[definition.id] = await ( + definition.multiselect ? prompts.checkbox : prompts.select + )({ + message: definition.message, + validate: (values) => { + if (!definition.validator) { + return true; + } + + return definition.validator(Object.values(values).map(({ value }) => value)); + }, + default: definition.multiselect ? undefined : definition.default, + choices: definition.items?.map((item) => + typeof item == 'string' + ? { + name: item, + value: item, + } + : { + ...item, + name: item.label, + value: item.value, + }, + ), + }); + break; + case 'input': { + let finalValue: JsonValue | undefined; + answers[definition.id] = await prompts.input({ + message: definition.message, + default: definition.default as string | undefined, + async validate(value) { + if (definition.validator === undefined) { + return true; + } + + let lastValidation: ReturnType = false; + for (const type of definition.propertyTypes) { + let potential; + switch (type) { + case 'string': + potential = String(value); + break; + case 'integer': + case 'number': + potential = Number(value); + break; + default: + potential = value; + break; + } + lastValidation = await definition.validator(potential); + + // Can be a string if validation fails + if (lastValidation === true) { + finalValue = potential; + + return true; + } + } + + return lastValidation; + }, + }); + + // Use validated value if present. + // This ensures the correct type is inserted into the final schema options. + if (finalValue !== undefined) { + answers[definition.id] = finalValue; + } + break; + } + } + } + + return answers; + }); + } + + return workflow; + } + + @memoize + protected async getSchematicCollections(): Promise> { + const getSchematicCollections = ( + configSection: Record | undefined, + ): Set | undefined => { + if (!configSection) { + return undefined; + } + + const { schematicCollections } = configSection; + if (Array.isArray(schematicCollections)) { + return new Set(schematicCollections); + } + + return undefined; + }; + + const { workspace, globalConfiguration } = this.context; + if (workspace) { + const project = getProjectByCwd(workspace); + if (project) { + const value = getSchematicCollections(workspace.getProjectCli(project)); + if (value) { + return value; + } + } + } + + const value = + getSchematicCollections(workspace?.getCli()) ?? + getSchematicCollections(globalConfiguration.getCli()); + if (value) { + return value; + } + + return new Set([DEFAULT_SCHEMATICS_COLLECTION]); + } + + protected parseSchematicInfo( + schematic: string | undefined, + ): [collectionName: string | undefined, schematicName: string | undefined] { + if (schematic?.includes(':')) { + const [collectionName, schematicName] = schematic.split(':', 2); + + return [collectionName, schematicName]; + } + + return [undefined, schematic]; + } + + protected async runSchematic(options: { + executionOptions: SchematicsExecutionOptions; + schematicOptions: OtherOptions; + collectionName: string; + schematicName: string; + }): Promise { + const { logger } = this.context; + const { schematicOptions, executionOptions, collectionName, schematicName } = options; + const workflow = await this.getOrCreateWorkflowForExecution(collectionName, executionOptions); + + if (!schematicName) { + throw new Error('schematicName cannot be undefined.'); + } + + const { unsubscribe, files } = subscribeToWorkflow(workflow, logger); + + try { + await workflow + .execute({ + collection: collectionName, + schematic: schematicName, + options: schematicOptions, + logger, + allowPrivate: this.allowPrivateSchematics, + }) + .toPromise(); + + if (!files.size) { + logger.info('Nothing to be done.'); + } + + if (executionOptions.dryRun) { + logger.warn(`\nNOTE: The "--dry-run" option means no changes were made.`); + } + } catch (err) { + // In case the workflow was not successful, show an appropriate error message. + if (err instanceof UnsuccessfulWorkflowExecution) { + // "See above" because we already printed the error. + logger.fatal('The Schematic workflow failed. See above.'); + } else { + assertIsError(err); + logger.fatal(err.message); + } + + return 1; + } finally { + unsubscribe(); + } + + return 0; + } + + private getProjectName(): string | undefined { + const { workspace } = this.context; + if (!workspace) { + return undefined; + } + + const projectName = getProjectByCwd(workspace); + if (projectName) { + return projectName; + } + + return undefined; + } + + private getResolvePaths(collectionName: string): string[] { + const { workspace, root } = this.context; + if (collectionName[0] === '.') { + // Resolve relative collections from the location of `angular.json` + return [root]; + } + + return workspace + ? // Workspace + collectionName === DEFAULT_SCHEMATICS_COLLECTION + ? // Favor __dirname for @schematics/angular to use the build-in version + [__dirname, process.cwd(), root] + : [process.cwd(), root, __dirname] + : // Global + [__dirname, process.cwd()]; + } +} diff --git a/packages/angular/cli/src/command-builder/utilities/command.ts b/packages/angular/cli/src/command-builder/utilities/command.ts new file mode 100644 index 000000000000..04a88c1f7113 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/command.ts @@ -0,0 +1,65 @@ +/** + * @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 { Argv } from 'yargs'; +import { + CommandContext, + CommandModule, + CommandModuleError, + CommandModuleImplementation, + CommandScope, +} from '../command-module'; + +export const demandCommandFailureMessage = `You need to specify a command before moving on. Use '--help' to view the available commands.`; +export type CommandModuleConstructor = Partial & { + new (context: CommandContext): Partial & CommandModule; +}; + +export function addCommandModuleToYargs( + localYargs: Argv, + commandModule: U, + context: CommandContext, +): Argv { + const cmd = new commandModule(context); + const { + args: { + options: { jsonHelp }, + }, + workspace, + } = context; + + const describe = jsonHelp ? cmd.fullDescribe : cmd.describe; + + return localYargs.command({ + command: cmd.command, + aliases: cmd.aliases, + describe: + // We cannot add custom fields in help, such as long command description which is used in AIO. + // Therefore, we get around this by adding a complex object as a string which we later parse when generating the help files. + typeof describe === 'object' ? JSON.stringify(describe) : describe, + deprecated: cmd.deprecated, + builder: (argv) => { + // Skip scope validation when running with '--json-help' since it's easier to generate the output for all commands this way. + const isInvalidScope = + !jsonHelp && + ((cmd.scope === CommandScope.In && !workspace) || + (cmd.scope === CommandScope.Out && workspace)); + + if (isInvalidScope) { + throw new CommandModuleError( + `This command is not available when running the Angular CLI ${ + workspace ? 'inside' : 'outside' + } a workspace.`, + ); + } + + return cmd.builder(argv) as Argv; + }, + handler: (args) => cmd.handler(args), + }); +} diff --git a/packages/angular/cli/src/command-builder/utilities/json-help.ts b/packages/angular/cli/src/command-builder/utilities/json-help.ts new file mode 100644 index 000000000000..6e673804ed84 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/json-help.ts @@ -0,0 +1,154 @@ +/** + * @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 yargs from 'yargs'; +import { FullDescribe } from '../command-module'; + +interface JsonHelpOption { + name: string; + type?: string; + deprecated: boolean | string; + aliases?: string[]; + default?: string; + required?: boolean; + positional?: number; + enum?: string[]; + description?: string; +} + +interface JsonHelpDescription { + shortDescription?: string; + longDescription?: string; + longDescriptionRelativePath?: string; +} + +interface JsonHelpSubcommand extends JsonHelpDescription { + name: string; + aliases: string[]; + deprecated: string | boolean; +} + +export interface JsonHelp extends JsonHelpDescription { + name: string; + command: string; + options: JsonHelpOption[]; + subcommands?: JsonHelpSubcommand[]; +} + +const yargsDefaultCommandRegExp = /^\$0|\*/; + +export function jsonHelpUsage(): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const localYargs = yargs as any; + const { + deprecatedOptions, + alias: aliases, + array, + string, + boolean, + number, + choices, + demandedOptions, + default: defaultVal, + hiddenOptions = [], + } = localYargs.getOptions(); + + const internalMethods = localYargs.getInternalMethods(); + const usageInstance = internalMethods.getUsageInstance(); + const context = internalMethods.getContext(); + const descriptions = usageInstance.getDescriptions(); + const groups = localYargs.getGroups(); + const positional = groups[usageInstance.getPositionalGroupName()] as string[] | undefined; + + const hidden = new Set(hiddenOptions); + const normalizeOptions: JsonHelpOption[] = []; + const allAliases = new Set([...Object.values(aliases).flat()]); + + for (const [names, type] of [ + [array, 'array'], + [string, 'string'], + [boolean, 'boolean'], + [number, 'number'], + ]) { + for (const name of names) { + if (allAliases.has(name) || hidden.has(name)) { + // Ignore hidden, aliases and already visited option. + continue; + } + + const positionalIndex = positional?.indexOf(name) ?? -1; + const alias = aliases[name]; + + normalizeOptions.push({ + name, + type, + deprecated: deprecatedOptions[name], + aliases: alias?.length > 0 ? alias : undefined, + default: defaultVal[name], + required: demandedOptions[name], + enum: choices[name], + description: descriptions[name]?.replace('__yargsString__:', ''), + positional: positionalIndex >= 0 ? positionalIndex : undefined, + }); + } + } + + // https://github.com/yargs/yargs/blob/00e4ebbe3acd438e73fdb101e75b4f879eb6d345/lib/usage.ts#L124 + const subcommands = ( + usageInstance.getCommands() as [ + name: string, + description: string, + isDefault: boolean, + aliases: string[], + deprecated: string | boolean, + ][] + ) + .map(([name, rawDescription, isDefault, aliases, deprecated]) => ({ + name: name.split(' ', 1)[0].replace(yargsDefaultCommandRegExp, ''), + command: name.replace(yargsDefaultCommandRegExp, ''), + default: isDefault || undefined, + ...parseDescription(rawDescription), + aliases, + deprecated, + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + const [command, rawDescription] = usageInstance.getUsage()[0] ?? []; + const defaultSubCommand = subcommands.find((x) => x.default)?.command ?? ''; + const otherSubcommands = subcommands.filter((s) => !s.default); + + const output: JsonHelp = { + name: [...context.commands].pop(), + command: `${command?.replace(yargsDefaultCommandRegExp, localYargs['$0'])}${defaultSubCommand}`, + ...parseDescription(rawDescription), + options: normalizeOptions.sort((a, b) => a.name.localeCompare(b.name)), + subcommands: otherSubcommands.length ? otherSubcommands : undefined, + }; + + return JSON.stringify(output, undefined, 2); +} + +function parseDescription(rawDescription: string): JsonHelpDescription { + try { + const { + longDescription, + describe: shortDescription, + longDescriptionRelativePath, + } = JSON.parse(rawDescription) as FullDescribe; + + return { + shortDescription, + longDescriptionRelativePath, + longDescription, + }; + } catch { + return { + shortDescription: rawDescription, + }; + } +} diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema.ts b/packages/angular/cli/src/command-builder/utilities/json-schema.ts new file mode 100644 index 000000000000..a46b06646197 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/json-schema.ts @@ -0,0 +1,358 @@ +/** + * @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 { json, strings } from '@angular-devkit/core'; +import yargs, { Arguments, Argv, PositionalOptions, Options as YargsOptions } from 'yargs'; + +/** + * An option description. + */ +export interface Option extends yargs.Options { + /** + * The name of the option. + */ + name: string; + + /** + * Whether this option is required or not. + */ + required?: boolean; + + /** + * Format field of this option. + */ + format?: string; + + /** + * Whether this option should be hidden from the help output. It will still show up in JSON help. + */ + hidden?: boolean; + + /** + * If this option can be used as an argument, the position of the argument. Otherwise omitted. + */ + positional?: number; + + /** + * Whether or not to report this option to the Angular Team, and which custom field to use. + * If this is falsey, do not report this option. + */ + userAnalytics?: string; + + /** + * Type of the values in a key/value pair field. + */ + itemValueType?: 'string'; +} + +function coerceToStringMap( + dashedName: string, + value: (string | undefined)[], +): Record | Promise { + const stringMap: Record = {}; + for (const pair of value) { + // This happens when the flag isn't passed at all. + if (pair === undefined) { + continue; + } + + const eqIdx = pair.indexOf('='); + if (eqIdx === -1) { + // TODO: Remove workaround once yargs properly handles thrown errors from coerce. + // Right now these sometimes end up as uncaught exceptions instead of proper validation + // errors with usage output. + return Promise.reject( + new Error( + `Invalid value for argument: ${dashedName}, Given: '${pair}', Expected key=value pair`, + ), + ); + } + const key = pair.slice(0, eqIdx); + const value = pair.slice(eqIdx + 1); + stringMap[key] = value; + } + + return stringMap; +} + +function isStringMap(node: json.JsonObject): boolean { + // Exclude fields with more specific kinds of properties. + if (node.properties || node.patternProperties) { + return false; + } + + // Restrict to additionalProperties with string values. + return ( + json.isJsonObject(node.additionalProperties) && + !node.additionalProperties.enum && + node.additionalProperties.type === 'string' + ); +} + +export async function parseJsonSchemaToOptions( + registry: json.schema.SchemaRegistry, + schema: json.JsonObject, + interactive = true, +): Promise { + const options: Option[] = []; + + function visitor( + current: json.JsonObject | json.JsonArray, + pointer: json.schema.JsonPointer, + parentSchema?: json.JsonObject | json.JsonArray, + ) { + if (!parentSchema) { + // Ignore root. + return; + } else if (pointer.split(/\/(?:properties|items|definitions)\//g).length > 2) { + // Ignore subitems (objects or arrays). + return; + } else if (json.isJsonArray(current)) { + return; + } + + if (pointer.indexOf('/not/') != -1) { + // We don't support anyOf/not. + throw new Error('The "not" keyword is not supported in JSON Schema.'); + } + + const ptr = json.schema.parseJsonPointer(pointer); + const name = ptr[ptr.length - 1]; + + if (ptr[ptr.length - 2] != 'properties') { + // Skip any non-property items. + return; + } + + const typeSet = json.schema.getTypesOfSchema(current); + + if (typeSet.size == 0) { + throw new Error('Cannot find type of schema.'); + } + + // We only support number, string or boolean (or array of those), so remove everything else. + const types = [...typeSet].filter((x) => { + switch (x) { + case 'boolean': + case 'number': + case 'string': + return true; + + case 'array': + // Only include arrays if they're boolean, string or number. + if ( + json.isJsonObject(current.items) && + typeof current.items.type == 'string' && + ['boolean', 'number', 'string'].includes(current.items.type) + ) { + return true; + } + + return false; + + case 'object': + return isStringMap(current); + + default: + return false; + } + }) as ('string' | 'number' | 'boolean' | 'array' | 'object')[]; + + if (types.length == 0) { + // This means it's not usable on the command line. e.g. an Object. + return; + } + + // Only keep enum values we support (booleans, numbers and strings). + const enumValues = ((json.isJsonArray(current.enum) && current.enum) || []).filter((x) => { + switch (typeof x) { + case 'boolean': + case 'number': + case 'string': + return true; + + default: + return false; + } + }) as (string | true | number)[]; + + let defaultValue: string | number | boolean | undefined = undefined; + if (current.default !== undefined) { + switch (types[0]) { + case 'string': + if (typeof current.default == 'string') { + defaultValue = current.default; + } + break; + case 'number': + if (typeof current.default == 'number') { + defaultValue = current.default; + } + break; + case 'boolean': + if (typeof current.default == 'boolean') { + defaultValue = current.default; + } + break; + } + } + + const $default = current.$default; + const $defaultIndex = + json.isJsonObject($default) && $default['$source'] == 'argv' ? $default['index'] : undefined; + const positional: number | undefined = + typeof $defaultIndex == 'number' ? $defaultIndex : undefined; + + let required = json.isJsonArray(schema.required) ? schema.required.includes(name) : false; + if (required && interactive && current['x-prompt']) { + required = false; + } + + const alias = json.isJsonArray(current.aliases) + ? [...current.aliases].map((x) => '' + x) + : current.alias + ? ['' + current.alias] + : []; + const format = typeof current.format == 'string' ? current.format : undefined; + const visible = current.visible === undefined || current.visible === true; + const hidden = !!current.hidden || !visible; + + const xUserAnalytics = current['x-user-analytics']; + const userAnalytics = typeof xUserAnalytics === 'string' ? xUserAnalytics : undefined; + + // Deprecated is set only if it's true or a string. + const xDeprecated = current['x-deprecated']; + const deprecated = + xDeprecated === true || typeof xDeprecated === 'string' ? xDeprecated : undefined; + + const option: Option = { + name, + description: '' + (current.description === undefined ? '' : current.description), + default: defaultValue, + choices: enumValues.length ? enumValues : undefined, + required, + alias, + format, + hidden, + userAnalytics, + deprecated, + positional, + ...(types[0] === 'object' + ? { + type: 'array', + itemValueType: 'string', + } + : { + type: types[0], + }), + }; + + options.push(option); + } + + const flattenedSchema = await registry.ɵflatten(schema); + json.schema.visitJsonSchema(flattenedSchema, visitor); + + // Sort by positional and name. + return options.sort((a, b) => { + if (a.positional) { + return b.positional ? a.positional - b.positional : a.name.localeCompare(b.name); + } else if (b.positional) { + return -1; + } + + return a.name.localeCompare(b.name); + }); +} + +/** + * Adds schema options to a command also this keeps track of options that are required for analytics. + * **Note:** This method should be called from the command bundler method. + * + * @returns A map from option name to analytics configuration. + */ +export function addSchemaOptionsToCommand( + localYargs: Argv, + options: Option[], + includeDefaultValues: boolean, +): Map { + const booleanOptionsWithNoPrefix = new Set(); + const keyValuePairOptions = new Set(); + const optionsWithAnalytics = new Map(); + + for (const option of options) { + const { + default: defaultVal, + positional, + deprecated, + description, + alias, + userAnalytics, + type, + itemValueType, + hidden, + name, + choices, + } = option; + + let dashedName = strings.dasherize(name); + + // Handle options which have been defined in the schema with `no` prefix. + if (type === 'boolean' && dashedName.startsWith('no-')) { + dashedName = dashedName.slice(3); + booleanOptionsWithNoPrefix.add(dashedName); + } + + if (itemValueType) { + keyValuePairOptions.add(name); + } + + const sharedOptions: YargsOptions & PositionalOptions = { + alias, + hidden, + description, + deprecated, + choices, + coerce: itemValueType ? coerceToStringMap.bind(null, dashedName) : undefined, + // This should only be done when `--help` is used otherwise default will override options set in angular.json. + ...(includeDefaultValues ? { default: defaultVal } : {}), + }; + + if (positional === undefined) { + localYargs = localYargs.option(dashedName, { + array: itemValueType ? true : undefined, + type: itemValueType ?? type, + ...sharedOptions, + }); + } else { + localYargs = localYargs.positional(dashedName, { + type: type === 'array' || type === 'count' ? 'string' : type, + ...sharedOptions, + }); + } + + // Record option of analytics. + if (userAnalytics !== undefined) { + optionsWithAnalytics.set(name, userAnalytics); + } + } + + // Handle options which have been defined in the schema with `no` prefix. + if (booleanOptionsWithNoPrefix.size) { + localYargs.middleware((options: Arguments) => { + for (const key of booleanOptionsWithNoPrefix) { + if (key in options) { + options[`no-${key}`] = !options[key]; + delete options[key]; + } + } + }, false); + } + + return optionsWithAnalytics; +} diff --git a/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts new file mode 100644 index 000000000000..5ec5db644bef --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts @@ -0,0 +1,221 @@ +/** + * @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 { json, schema } from '@angular-devkit/core'; +import yargs, { positional } from 'yargs'; + +import { addSchemaOptionsToCommand, parseJsonSchemaToOptions } from './json-schema'; + +const YError = (() => { + try { + const y = yargs().strict().fail(false).exitProcess(false).parse(['--forced-failure']); + } catch (e) { + if (!(e instanceof Error)) { + throw new Error('Unexpected non-Error thrown'); + } + + return e.constructor as typeof Error; + } + throw new Error('Expected parse to fail'); +})(); + +interface ParseFunction { + (argv: string[]): unknown; +} + +function withParseForSchema( + jsonSchema: json.JsonObject, + { + interactive = true, + includeDefaultValues = true, + }: { interactive?: boolean; includeDefaultValues?: boolean } = {}, +): ParseFunction { + let actualParse: ParseFunction = () => { + throw new Error('Called before init'); + }; + const parse: ParseFunction = (args) => { + return actualParse(args); + }; + + beforeEach(async () => { + const registry = new schema.CoreSchemaRegistry(); + const options = await parseJsonSchemaToOptions(registry, jsonSchema, interactive); + + actualParse = async (args: string[]) => { + // Create a fresh yargs for each call. The yargs object is stateful and + // calling .parse multiple times on the same instance isn't safe. + const localYargs = yargs().exitProcess(false).strict().fail(false); + addSchemaOptionsToCommand(localYargs, options, includeDefaultValues); + + // Yargs only exposes the parse errors as proper errors when using the + // callback syntax. This unwraps that ugly workaround so tests can just + // use simple .toThrow/.toEqual assertions. + return localYargs.parseAsync(args); + }; + }); + + return parse; +} + +describe('parseJsonSchemaToOptions', () => { + describe('without required fields in schema', () => { + const parse = withParseForSchema({ + 'type': 'object', + 'properties': { + 'maxSize': { + 'type': 'number', + }, + 'ssr': { + 'type': 'string', + 'enum': ['always', 'surprise-me', 'never'], + }, + 'extendable': { + 'type': 'object', + 'properties': {}, + 'additionalProperties': { + 'type': 'string', + }, + }, + 'someDefine': { + 'type': 'object', + 'additionalProperties': { + 'type': 'string', + }, + }, + }, + }); + + describe('type=number', () => { + it('parses valid option value', async () => { + expect(await parse(['--max-size', '42'])).toEqual( + jasmine.objectContaining({ + 'maxSize': 42, + }), + ); + }); + }); + + describe('type=string, enum', () => { + it('parses valid option value', async () => { + expect(await parse(['--ssr', 'never'])).toEqual( + jasmine.objectContaining({ + 'ssr': 'never', + }), + ); + }); + + it('rejects non-enum values', async () => { + await expectAsync(parse(['--ssr', 'yes'])).toBeRejectedWithError( + /Argument: ssr, Given: "yes", Choices:/, + ); + }); + }); + + describe('type=object', () => { + it('ignores fields that define specific properties', async () => { + await expectAsync(parse(['--extendable', 'a=b'])).toBeRejectedWithError( + /Unknown argument: extendable/, + ); + }); + + it('rejects invalid values for string maps', async () => { + await expectAsync(parse(['--some-define', 'foo'])).toBeRejectedWithError( + YError, + /Invalid value for argument: some-define, Given: 'foo', Expected key=value pair/, + ); + await expectAsync(parse(['--some-define', '42'])).toBeRejectedWithError( + YError, + /Invalid value for argument: some-define, Given: '42', Expected key=value pair/, + ); + }); + + it('aggregates an object value', async () => { + expect( + await parse([ + '--some-define', + 'A_BOOLEAN=true', + '--some-define', + 'AN_INTEGER=42', + // Ensure we can handle '=' inside of string values. + '--some-define=A_STRING="❤️=❤️"', + '--some-define', + 'AN_UNQUOTED_STRING=❤️=❤️', + ]), + ).toEqual( + jasmine.objectContaining({ + 'someDefine': { + 'A_BOOLEAN': 'true', + 'AN_INTEGER': '42', + 'A_STRING': '"❤️=❤️"', + 'AN_UNQUOTED_STRING': '❤️=❤️', + }, + }), + ); + }); + }); + }); + + describe('with required positional argument', () => { + it('marks the required argument as required', async () => { + const jsonSchema = JSON.parse(` + { + "$id": "FakeSchema", + "title": "Fake Schema", + "type": "object", + "required": ["a"], + "properties": { + "b": { + "type": "string", + "description": "b.", + "$default": { + "$source": "argv", + "index": 1 + } + }, + "a": { + "type": "string", + "description": "a.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "optC": { + "type": "string", + "description": "optC" + }, + "optA": { + "type": "string", + "description": "optA" + }, + "optB": { + "type": "string", + "description": "optB" + } + } + }`) as json.JsonObject; + const registry = new schema.CoreSchemaRegistry(); + const options = await parseJsonSchemaToOptions(registry, jsonSchema, /* interactive= */ true); + + expect(options.find((opt) => opt.name === 'a')).toEqual( + jasmine.objectContaining({ + name: 'a', + positional: 0, + required: true, + }), + ); + expect(options.find((opt) => opt.name === 'b')).toEqual( + jasmine.objectContaining({ + name: 'b', + positional: 1, + required: false, + }), + ); + }); + }); +}); diff --git a/packages/angular/cli/src/command-builder/utilities/normalize-options-middleware.ts b/packages/angular/cli/src/command-builder/utilities/normalize-options-middleware.ts new file mode 100644 index 000000000000..709f9e5a7c67 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/normalize-options-middleware.ts @@ -0,0 +1,37 @@ +/** + * @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 yargs from 'yargs'; + +/** + * A Yargs middleware that normalizes non Array options when the argument has been provided multiple times. + * + * By default, when an option is non array and it is provided multiple times in the command line, yargs + * will not override it's value but instead it will be changed to an array unless `duplicate-arguments-array` is disabled. + * But this option also have an effect on real array options which isn't desired. + * + * See: https://github.com/yargs/yargs-parser/pull/163#issuecomment-516566614 + */ +export function normalizeOptionsMiddleware(args: yargs.Arguments): void { + // `getOptions` is not included in the types even though it's public API. + // https://github.com/yargs/yargs/issues/2098 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { array } = (yargs as any).getOptions(); + const arrayOptions = new Set(array); + + for (const [key, value] of Object.entries(args)) { + if (key !== '_' && Array.isArray(value) && !arrayOptions.has(key)) { + const newValue = value.pop(); + // eslint-disable-next-line no-console + console.warn( + `Option '${key}' has been specified multiple times. The value '${newValue}' will be used.`, + ); + args[key] = newValue; + } + } +} diff --git a/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts b/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts new file mode 100644 index 000000000000..e4b805f1a367 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts @@ -0,0 +1,228 @@ +/** + * @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 { RuleFactory, SchematicsException, Tree } from '@angular-devkit/schematics'; +import { FileSystemCollectionDesc, NodeModulesEngineHost } from '@angular-devkit/schematics/tools'; +import { parse as parseJson } from 'jsonc-parser'; +import { readFileSync } from 'node:fs'; +import { Module, createRequire } from 'node:module'; +import { dirname, resolve } from 'node:path'; +import { Script } from 'node:vm'; +import { assertIsError } from '../../utilities/error'; + +/** + * Environment variable to control schematic package redirection + */ +const schematicRedirectVariable = process.env['NG_SCHEMATIC_REDIRECT']?.toLowerCase(); + +function shouldWrapSchematic(schematicFile: string, schematicEncapsulation: boolean): boolean { + // Check environment variable if present + switch (schematicRedirectVariable) { + case '0': + case 'false': + case 'off': + case 'none': + return false; + case 'all': + return true; + } + + const normalizedSchematicFile = schematicFile.replace(/\\/g, '/'); + // Never wrap the internal update schematic when executed directly + // It communicates with the update command via `global` + // But we still want to redirect schematics located in `@angular/cli/node_modules`. + if ( + normalizedSchematicFile.includes('node_modules/@angular/cli/') && + !normalizedSchematicFile.includes('node_modules/@angular/cli/node_modules/') + ) { + return false; + } + + // @angular/pwa uses dynamic imports which causes `[1] 2468039 segmentation fault` when wrapped. + // We should remove this when make `importModuleDynamically` work. + // See: https://nodejs.org/docs/latest-v14.x/api/vm.html + if (normalizedSchematicFile.includes('@angular/pwa')) { + return false; + } + + // Check for first-party Angular schematic packages + // Angular schematics are safe to use in the wrapped VM context + if (/\/node_modules\/@(?:angular|schematics|nguniversal)\//.test(normalizedSchematicFile)) { + return true; + } + + // Otherwise use the value of the schematic collection's encapsulation option (current default of false) + return schematicEncapsulation; +} + +export class SchematicEngineHost extends NodeModulesEngineHost { + protected override _resolveReferenceString( + refString: string, + parentPath: string, + collectionDescription?: FileSystemCollectionDesc, + ) { + const [path, name] = refString.split('#', 2); + // Mimic behavior of ExportStringRef class used in default behavior + const fullPath = path[0] === '.' ? resolve(parentPath ?? process.cwd(), path) : path; + + const referenceRequire = createRequire(__filename); + const schematicFile = referenceRequire.resolve(fullPath, { paths: [parentPath] }); + + if (shouldWrapSchematic(schematicFile, !!collectionDescription?.encapsulation)) { + const schematicPath = dirname(schematicFile); + + const moduleCache = new Map(); + const factoryInitializer = wrap( + schematicFile, + schematicPath, + moduleCache, + name || 'default', + ) as () => RuleFactory<{}>; + + const factory = factoryInitializer(); + if (!factory || typeof factory !== 'function') { + return null; + } + + return { ref: factory, path: schematicPath }; + } + + // All other schematics use default behavior + return super._resolveReferenceString(refString, parentPath, collectionDescription); + } +} + +/** + * Minimal shim modules for legacy deep imports of `@schematics/angular` + */ +const legacyModules: Record = { + '@schematics/angular/utility/config': { + getWorkspace(host: Tree) { + const path = '/.angular.json'; + const data = host.read(path); + if (!data) { + throw new SchematicsException(`Could not find (${path})`); + } + + return parseJson(data.toString(), [], { allowTrailingComma: true }); + }, + }, + '@schematics/angular/utility/project': { + buildDefaultPath(project: { sourceRoot?: string; root: string; projectType: string }): string { + const root = project.sourceRoot ? `/${project.sourceRoot}/` : `/${project.root}/src/`; + + return `${root}${project.projectType === 'application' ? 'app' : 'lib'}`; + }, + }, +}; + +/** + * Wrap a JavaScript file in a VM context to allow specific Angular dependencies to be redirected. + * This VM setup is ONLY intended to redirect dependencies. + * + * @param schematicFile A JavaScript schematic file path that should be wrapped. + * @param schematicDirectory A directory that will be used as the location of the JavaScript file. + * @param moduleCache A map to use for caching repeat module usage and proper `instanceof` support. + * @param exportName An optional name of a specific export to return. Otherwise, return all exports. + */ +function wrap( + schematicFile: string, + schematicDirectory: string, + moduleCache: Map, + exportName?: string, +): () => unknown { + const hostRequire = createRequire(__filename); + const schematicRequire = createRequire(schematicFile); + + const customRequire = function (id: string) { + if (legacyModules[id]) { + // Provide compatibility modules for older versions of @angular/cdk + return legacyModules[id]; + } else if (id.startsWith('schematics:')) { + // Schematics built-in modules use the `schematics` scheme (similar to the Node.js `node` scheme) + const builtinId = id.slice(11); + const builtinModule = loadBuiltinModule(builtinId); + if (!builtinModule) { + throw new Error( + `Unknown schematics built-in module '${id}' requested from schematic '${schematicFile}'`, + ); + } + + return builtinModule; + } else if (id.startsWith('@angular-devkit/') || id.startsWith('@schematics/')) { + // Files should not redirect `@angular/core` and instead use the direct + // dependency if available. This allows old major version migrations to continue to function + // even though the latest major version may have breaking changes in `@angular/core`. + if (id.startsWith('@angular-devkit/core')) { + try { + return schematicRequire(id); + } catch (e) { + assertIsError(e); + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } + } + } + + // Resolve from inside the `@angular/cli` project + return hostRequire(id); + } else if (id.startsWith('.') || id.startsWith('@angular/cdk')) { + // Wrap relative files inside the schematic collection + // Also wrap `@angular/cdk`, it contains helper utilities that import core schematic packages + + // Resolve from the original file + const modulePath = schematicRequire.resolve(id); + + // Use cached module if available + const cachedModule = moduleCache.get(modulePath); + if (cachedModule) { + return cachedModule; + } + + // Do not wrap vendored third-party packages or JSON files + if ( + !/[/\\]node_modules[/\\]@schematics[/\\]angular[/\\]third_party[/\\]/.test(modulePath) && + !modulePath.endsWith('.json') + ) { + // Wrap module and save in cache + const wrappedModule = wrap(modulePath, dirname(modulePath), moduleCache)(); + moduleCache.set(modulePath, wrappedModule); + + return wrappedModule; + } + } + + // All others are required directly from the original file + return schematicRequire(id); + }; + + // Setup a wrapper function to capture the module's exports + const schematicCode = readFileSync(schematicFile, 'utf8'); + const script = new Script(Module.wrap(schematicCode), { + filename: schematicFile, + lineOffset: 1, + }); + const schematicModule = new Module(schematicFile); + const moduleFactory = script.runInThisContext(); + + return () => { + moduleFactory( + schematicModule.exports, + customRequire, + schematicModule, + schematicFile, + schematicDirectory, + ); + + return exportName ? schematicModule.exports[exportName] : schematicModule.exports; + }; +} + +function loadBuiltinModule(id: string): unknown { + return undefined; +} diff --git a/packages/angular/cli/src/command-builder/utilities/schematic-workflow.ts b/packages/angular/cli/src/command-builder/utilities/schematic-workflow.ts new file mode 100644 index 000000000000..f5caa0754d88 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/schematic-workflow.ts @@ -0,0 +1,79 @@ +/** + * @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 { NodeWorkflow } from '@angular-devkit/schematics/tools'; +import { colors } from '../../utilities/color'; + +function removeLeadingSlash(value: string): string { + return value[0] === '/' ? value.slice(1) : value; +} + +export function subscribeToWorkflow( + workflow: NodeWorkflow, + logger: logging.LoggerApi, +): { + files: Set; + error: boolean; + unsubscribe: () => void; +} { + const files = new Set(); + let error = false; + let logs: string[] = []; + + const reporterSubscription = workflow.reporter.subscribe((event) => { + // Strip leading slash to prevent confusion. + const eventPath = removeLeadingSlash(event.path); + + switch (event.kind) { + case 'error': + error = true; + logger.error( + `ERROR! ${eventPath} ${event.description == 'alreadyExist' ? 'already exists' : 'does not exist'}.`, + ); + break; + case 'update': + logs.push(`${colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)`); + files.add(eventPath); + break; + case 'create': + logs.push(`${colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`); + files.add(eventPath); + break; + case 'delete': + logs.push(`${colors.yellow('DELETE')} ${eventPath}`); + files.add(eventPath); + break; + case 'rename': + logs.push(`${colors.blue('RENAME')} ${eventPath} => ${removeLeadingSlash(event.to)}`); + files.add(eventPath); + break; + } + }); + + const lifecycleSubscription = workflow.lifeCycle.subscribe((event) => { + if (event.kind == 'end' || event.kind == 'post-tasks-start') { + if (!error) { + // Output the logging queue, no error happened. + logs.forEach((log) => logger.info(log)); + } + + logs = []; + error = false; + } + }); + + return { + files, + error, + unsubscribe: () => { + reporterSubscription.unsubscribe(); + lifecycleSubscription.unsubscribe(); + }, + }; +} diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts new file mode 100644 index 000000000000..adaf980cd4b3 --- /dev/null +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -0,0 +1,554 @@ +/** + * @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 { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools'; +import { Listr, color, figures } from 'listr2'; +import assert from 'node:assert'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import npa from 'npm-package-arg'; +import { Range, compare, intersects, prerelease, satisfies, valid } from 'semver'; +import { Argv } from 'yargs'; +import { PackageManager } from '../../../lib/config/workspace-schema'; +import { + CommandModuleImplementation, + Options, + OtherOptions, +} from '../../command-builder/command-module'; +import { + SchematicsCommandArgs, + SchematicsCommandModule, +} from '../../command-builder/schematics-command-module'; +import { assertIsError } from '../../utilities/error'; +import { + NgAddSaveDependency, + PackageManifest, + fetchPackageManifest, + fetchPackageMetadata, +} from '../../utilities/package-metadata'; +import { isTTY } from '../../utilities/tty'; +import { VERSION } from '../../utilities/version'; + +class CommandError extends Error {} + +interface AddCommandArgs extends SchematicsCommandArgs { + collection: string; + verbose?: boolean; + registry?: string; + 'skip-confirmation'?: boolean; +} + +interface AddCommandTaskContext { + packageIdentifier: npa.Result; + usingYarn?: boolean; + savePackage?: NgAddSaveDependency; + collectionName?: string; + executeSchematic: AddCommandModule['executeSchematic']; + hasMismatchedPeer: AddCommandModule['hasMismatchedPeer']; +} + +/** + * The set of packages that should have certain versions excluded from consideration + * when attempting to find a compatible version for a package. + * The key is a package name and the value is a SemVer range of versions to exclude. + */ +const packageVersionExclusions: Record = { + // @angular/localize@9.x and earlier versions as well as @angular/localize@10.0 prereleases do not have peer dependencies setup. + '@angular/localize': '<10.0.0', + // @angular/material@7.x versions have unbounded peer dependency ranges (>=7.0.0). + '@angular/material': '7.x', +}; + +export default class AddCommandModule + extends SchematicsCommandModule + implements CommandModuleImplementation +{ + command = 'add '; + describe = 'Adds support for an external library to your project.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + protected override allowPrivateSchematics = true; + private readonly schematicName = 'ng-add'; + private rootRequire = createRequire(this.context.root + '/'); + + override async builder(argv: Argv): Promise> { + const localYargs = (await super.builder(argv)) + .positional('collection', { + description: 'The package to be added.', + type: 'string', + demandOption: true, + }) + .option('registry', { description: 'The NPM registry to use.', type: 'string' }) + .option('verbose', { + description: 'Display additional details about internal operations during execution.', + type: 'boolean', + default: false, + }) + .option('skip-confirmation', { + description: + 'Skip asking a confirmation prompt before installing and executing the package. ' + + 'Ensure package name is correct prior to using this option.', + type: 'boolean', + default: false, + }) + // Prior to downloading we don't know the full schema and therefore we cannot be strict on the options. + // Possibly in the future update the logic to use the following syntax: + // `ng add @angular/localize -- --package-options`. + .strict(false); + + const collectionName = this.getCollectionName(); + if (!collectionName) { + return localYargs; + } + + const workflow = this.getOrCreateWorkflowForBuilder(collectionName); + + try { + const collection = workflow.engine.createCollection(collectionName); + const options = await this.getSchematicOptions(collection, this.schematicName, workflow); + + return this.addSchemaOptionsToCommand(localYargs, options); + } catch (error) { + // During `ng add` prior to the downloading of the package + // we are not able to resolve and create a collection. + // Or when the collection value is a path to a tarball. + } + + return localYargs; + } + + // eslint-disable-next-line max-lines-per-function + async run(options: Options & OtherOptions): Promise { + const { logger, packageManager } = this.context; + const { verbose, registry, collection, skipConfirmation } = options; + + let packageIdentifier; + try { + packageIdentifier = npa(collection); + } catch (e) { + assertIsError(e); + logger.error(e.message); + + return 1; + } + + if ( + packageIdentifier.name && + packageIdentifier.registry && + this.isPackageInstalled(packageIdentifier.name) + ) { + const validVersion = await this.isProjectVersionValid(packageIdentifier); + if (validVersion) { + // Already installed so just run schematic + logger.info('Skipping installation: Package already installed'); + + return this.executeSchematic({ ...options, collection: packageIdentifier.name }); + } + } + + const taskContext: AddCommandTaskContext = { + packageIdentifier, + executeSchematic: this.executeSchematic.bind(this), + hasMismatchedPeer: this.hasMismatchedPeer.bind(this), + }; + + const tasks = new Listr([ + { + title: 'Determining Package Manager', + task(context, task) { + context.usingYarn = packageManager.name === PackageManager.Yarn; + task.output = `Using package manager: ${color.dim(packageManager.name)}`; + }, + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Searching for compatible package version', + enabled: packageIdentifier.type === 'range' && packageIdentifier.rawSpec === '*', + async task(context, task) { + assert( + context.packageIdentifier.name, + 'Registry package identifiers should always have a name.', + ); + + // only package name provided; search for viable version + // plus special cases for packages that did not have peer deps setup + let packageMetadata; + try { + packageMetadata = await fetchPackageMetadata(context.packageIdentifier.name, logger, { + registry, + usingYarn: context.usingYarn, + verbose, + }); + } catch (e) { + assertIsError(e); + throw new CommandError( + `Unable to load package information from registry: ${e.message}`, + ); + } + + // Start with the version tagged as `latest` if it exists + const latestManifest = packageMetadata.tags['latest']; + if (latestManifest) { + context.packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version); + } + + // Adjust the version based on name and peer dependencies + if ( + latestManifest?.peerDependencies && + Object.keys(latestManifest.peerDependencies).length === 0 + ) { + task.output = `Found compatible package version: ${color.blue(latestManifest.version)}.`; + } else if (!latestManifest || (await context.hasMismatchedPeer(latestManifest))) { + // 'latest' is invalid so search for most recent matching package + + // Allow prelease versions if the CLI itself is a prerelease + const allowPrereleases = prerelease(VERSION.full); + + const versionExclusions = packageVersionExclusions[packageMetadata.name]; + const versionManifests = Object.values(packageMetadata.versions).filter( + (value: PackageManifest) => { + // Prerelease versions are not stable and should not be considered by default + if (!allowPrereleases && prerelease(value.version)) { + return false; + } + // Deprecated versions should not be used or considered + if (value.deprecated) { + return false; + } + // Excluded package versions should not be considered + if ( + versionExclusions && + satisfies(value.version, versionExclusions, { includePrerelease: true }) + ) { + return false; + } + + return true; + }, + ); + + // Sort in reverse SemVer order so that the newest compatible version is chosen + versionManifests.sort((a, b) => compare(b.version, a.version, true)); + + let found = false; + for (const versionManifest of versionManifests) { + const mismatch = await context.hasMismatchedPeer(versionManifest); + if (mismatch) { + continue; + } + + context.packageIdentifier = npa.resolve( + versionManifest.name, + versionManifest.version, + ); + found = true; + break; + } + + if (!found) { + task.output = "Unable to find compatible package. Using 'latest' tag."; + } else { + task.output = `Found compatible package version: ${color.blue(context.packageIdentifier.toString())}.`; + } + } else { + task.output = `Found compatible package version: ${color.blue(context.packageIdentifier.toString())}.`; + } + }, + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Loading package information from registry', + async task(context, task) { + let manifest; + try { + manifest = await fetchPackageManifest(context.packageIdentifier.toString(), logger, { + registry, + verbose, + usingYarn: context.usingYarn, + }); + } catch (e) { + assertIsError(e); + throw new CommandError( + `Unable to fetch package information for '${context.packageIdentifier}': ${e.message}`, + ); + } + + context.savePackage = manifest['ng-add']?.save; + context.collectionName = manifest.name; + + if (await context.hasMismatchedPeer(manifest)) { + task.output = color.yellow( + figures.warning + + ' Package has unmet peer dependencies. Adding the package may not succeed.', + ); + } + }, + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Confirming installation', + enabled: !skipConfirmation, + async task(context, task) { + if (!isTTY()) { + task.output = + `'--skip-confirmation' can be used to bypass installation confirmation. ` + + `Ensure package name is correct prior to '--skip-confirmation' option usage.`; + throw new CommandError('No terminal detected'); + } + + const { ListrInquirerPromptAdapter } = await import('@listr2/prompt-adapter-inquirer'); + const { confirm } = await import('@inquirer/prompts'); + const shouldProceed = await task.prompt(ListrInquirerPromptAdapter).run(confirm, { + message: + `The package ${color.blue(context.packageIdentifier.toString())} will be installed and executed.\n` + + 'Would you like to proceed?', + default: true, + theme: { prefix: '' }, + }); + + if (!shouldProceed) { + throw new CommandError('Command aborted'); + } + }, + rendererOptions: { persistentOutput: true }, + }, + { + async task(context, task) { + // Only show if installation will actually occur + task.title = 'Installing package'; + + if (context.savePackage === false) { + task.title += ' in temporary location'; + + // Temporary packages are located in a different directory + // Hence we need to resolve them using the temp path + const { success, tempNodeModules } = await packageManager.installTemp( + context.packageIdentifier.toString(), + registry ? [`--registry="${registry}"`] : undefined, + ); + const tempRequire = createRequire(tempNodeModules + '/'); + assert(context.collectionName, 'Collection name should always be available'); + const resolvedCollectionPath = tempRequire.resolve( + join(context.collectionName, 'package.json'), + ); + + if (!success) { + throw new CommandError('Unable to install package'); + } + + context.collectionName = dirname(resolvedCollectionPath); + } else { + const success = await packageManager.install( + context.packageIdentifier.toString(), + context.savePackage, + registry ? [`--registry="${registry}"`] : undefined, + undefined, + ); + + if (!success) { + throw new CommandError('Unable to install package'); + } + } + }, + rendererOptions: { bottomBar: Infinity }, + }, + // TODO: Rework schematic execution as a task and insert here + ]); + + try { + const result = await tasks.run(taskContext); + assert(result.collectionName, 'Collection name should always be available'); + + return this.executeSchematic({ ...options, collection: result.collectionName }); + } catch (e) { + if (e instanceof CommandError) { + return 1; + } + + throw e; + } + } + + private async isProjectVersionValid(packageIdentifier: npa.Result): Promise { + if (!packageIdentifier.name) { + return false; + } + + const installedVersion = await this.findProjectVersion(packageIdentifier.name); + if (!installedVersion) { + return false; + } + + if (packageIdentifier.rawSpec === '*') { + return true; + } + + if ( + packageIdentifier.type === 'range' && + packageIdentifier.fetchSpec && + packageIdentifier.fetchSpec !== '*' + ) { + return satisfies(installedVersion, packageIdentifier.fetchSpec); + } + + if (packageIdentifier.type === 'version') { + const v1 = valid(packageIdentifier.fetchSpec); + const v2 = valid(installedVersion); + + return v1 !== null && v1 === v2; + } + + return false; + } + + private getCollectionName(): string | undefined { + const [, collectionName] = this.context.args.positional; + if (!collectionName) { + return undefined; + } + + // The CLI argument may specify also a version, like `ng add @my/lib@13.0.0`, + // but here we need only the name of the package, like `@my/lib`. + try { + const packageName = npa(collectionName).name; + if (packageName) { + return packageName; + } + } catch (e) { + assertIsError(e); + this.context.logger.error(e.message); + } + + return collectionName; + } + + private isPackageInstalled(name: string): boolean { + try { + this.rootRequire.resolve(join(name, 'package.json')); + + return true; + } catch (e) { + assertIsError(e); + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } + } + + return false; + } + + private async executeSchematic( + options: Options & OtherOptions, + ): Promise { + try { + const { + verbose, + skipConfirmation, + interactive, + force, + dryRun, + registry, + defaults, + collection: collectionName, + ...schematicOptions + } = options; + + return await this.runSchematic({ + schematicOptions, + schematicName: this.schematicName, + collectionName, + executionOptions: { + interactive, + force, + dryRun, + defaults, + packageRegistry: registry, + }, + }); + } catch (e) { + if (e instanceof NodePackageDoesNotSupportSchematics) { + this.context.logger.error( + 'The package that you are trying to add does not support schematics.' + + 'You can try using a different version of the package or contact the package author to add ng-add support.', + ); + + return 1; + } + + throw e; + } + } + + private async findProjectVersion(name: string): Promise { + const { logger, root } = this.context; + let installedPackage; + try { + installedPackage = this.rootRequire.resolve(join(name, 'package.json')); + } catch {} + + if (installedPackage) { + try { + const installed = await fetchPackageManifest(dirname(installedPackage), logger); + + return installed.version; + } catch {} + } + + let projectManifest; + try { + projectManifest = await fetchPackageManifest(root, logger); + } catch {} + + if (projectManifest) { + const version = + projectManifest.dependencies?.[name] || projectManifest.devDependencies?.[name]; + if (version) { + return version; + } + } + + return null; + } + + private async hasMismatchedPeer(manifest: PackageManifest): Promise { + for (const peer in manifest.peerDependencies) { + let peerIdentifier; + try { + peerIdentifier = npa.resolve(peer, manifest.peerDependencies[peer]); + } catch { + this.context.logger.warn(`Invalid peer dependency ${peer} found in package.`); + continue; + } + + if (peerIdentifier.type === 'version' || peerIdentifier.type === 'range') { + try { + const version = await this.findProjectVersion(peer); + if (!version) { + continue; + } + + const options = { includePrerelease: true }; + + if ( + !intersects(version, peerIdentifier.rawSpec, options) && + !satisfies(version, peerIdentifier.rawSpec, options) + ) { + return true; + } + } catch { + // Not found or invalid so ignore + continue; + } + } else { + // type === 'tag' | 'file' | 'directory' | 'remote' | 'git' + // Cannot accurately compare these as the tag/location may have changed since install + } + } + + return false; + } +} diff --git a/packages/angular/cli/src/commands/add/long-description.md b/packages/angular/cli/src/commands/add/long-description.md new file mode 100644 index 000000000000..347b3a5971aa --- /dev/null +++ b/packages/angular/cli/src/commands/add/long-description.md @@ -0,0 +1,7 @@ +Adds the npm package for a published library to your workspace, and configures +the project in the current working directory to use that library, as specified by the library's schematic. +For example, adding `@angular/pwa` configures your project for PWA support: + +```bash +ng add @angular/pwa +``` diff --git a/packages/angular/cli/src/commands/analytics/cli.ts b/packages/angular/cli/src/commands/analytics/cli.ts new file mode 100644 index 000000000000..56841a95bd6b --- /dev/null +++ b/packages/angular/cli/src/commands/analytics/cli.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 { join } from 'node:path'; +import { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleImplementation, + Options, +} from '../../command-builder/command-module'; +import { + addCommandModuleToYargs, + demandCommandFailureMessage, +} from '../../command-builder/utilities/command'; +import { AnalyticsInfoCommandModule } from './info/cli'; +import { + AnalyticsDisableModule, + AnalyticsEnableModule, + AnalyticsPromptModule, +} from './settings/cli'; + +export default class AnalyticsCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'analytics'; + describe = 'Configures the gathering of Angular CLI usage metrics.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + + builder(localYargs: Argv): Argv { + const subcommands = [ + AnalyticsInfoCommandModule, + AnalyticsDisableModule, + AnalyticsEnableModule, + AnalyticsPromptModule, + ].sort(); // sort by class name. + + for (const module of subcommands) { + localYargs = addCommandModuleToYargs(localYargs, module, this.context); + } + + return localYargs.demandCommand(1, demandCommandFailureMessage).strict(); + } + + run(_options: Options<{}>): void {} +} diff --git a/packages/angular/cli/src/commands/analytics/info/cli.ts b/packages/angular/cli/src/commands/analytics/info/cli.ts new file mode 100644 index 000000000000..e4434d35baee --- /dev/null +++ b/packages/angular/cli/src/commands/analytics/info/cli.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 { Argv } from 'yargs'; +import { getAnalyticsInfoString } from '../../../analytics/analytics'; +import { + CommandModule, + CommandModuleImplementation, + Options, +} from '../../../command-builder/command-module'; + +export class AnalyticsInfoCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'info'; + describe = 'Prints analytics gathering and reporting configuration in the console.'; + longDescriptionPath?: string; + + builder(localYargs: Argv): Argv { + return localYargs.strict(); + } + + async run(_options: Options<{}>): Promise { + this.context.logger.info(await getAnalyticsInfoString(this.context)); + } +} diff --git a/packages/angular/cli/src/commands/analytics/long-description.md b/packages/angular/cli/src/commands/analytics/long-description.md new file mode 100644 index 000000000000..69ee9ad7ee00 --- /dev/null +++ b/packages/angular/cli/src/commands/analytics/long-description.md @@ -0,0 +1,20 @@ +You can help the Angular Team to prioritize features and improvements by permitting the Angular team to send command-line command usage statistics to Google. +The Angular Team does not collect usage statistics unless you explicitly opt in. When installing the Angular CLI you are prompted to allow global collection of usage statistics. +If you say no or skip the prompt, no data is collected. + +### What is collected? + +Usage analytics include the commands and selected flags for each execution. +Usage analytics may include the following information: + +- Your operating system \(macOS, Linux distribution, Windows\) and its version. +- Package manager name and version \(local version only\). +- Node.js version \(local version only\). +- Angular CLI version \(local version only\). +- Command name that was run. +- Workspace information, the number of application and library projects. +- For schematics commands \(add, generate and new\), the schematic collection and name and a list of selected flags. +- For build commands \(build, serve\), the builder name, the number and size of bundles \(initial and lazy\), compilation units, the time it took to build and rebuild, and basic Angular-specific API usage. + +Only Angular owned and developed schematics and builders are reported. +Third-party schematics and builders do not send data to the Angular Team. diff --git a/packages/angular/cli/src/commands/analytics/settings/cli.ts b/packages/angular/cli/src/commands/analytics/settings/cli.ts new file mode 100644 index 000000000000..16f07b353d1a --- /dev/null +++ b/packages/angular/cli/src/commands/analytics/settings/cli.ts @@ -0,0 +1,82 @@ +/** + * @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 { Argv } from 'yargs'; +import { + getAnalyticsInfoString, + promptAnalytics, + setAnalyticsConfig, +} from '../../../analytics/analytics'; +import { + CommandModule, + CommandModuleImplementation, + Options, +} from '../../../command-builder/command-module'; + +interface AnalyticsCommandArgs { + global: boolean; +} + +abstract class AnalyticsSettingModule + extends CommandModule + implements CommandModuleImplementation +{ + longDescriptionPath?: string; + + builder(localYargs: Argv): Argv { + return localYargs + .option('global', { + description: `Configure analytics gathering and reporting globally in the caller's home directory.`, + alias: ['g'], + type: 'boolean', + default: false, + }) + .strict(); + } + + abstract override run({ global }: Options): Promise; +} + +export class AnalyticsDisableModule + extends AnalyticsSettingModule + implements CommandModuleImplementation +{ + command = 'disable'; + aliases = 'off'; + describe = 'Disables analytics gathering and reporting for the user.'; + + async run({ global }: Options): Promise { + await setAnalyticsConfig(global, false); + process.stderr.write(await getAnalyticsInfoString(this.context)); + } +} + +export class AnalyticsEnableModule + extends AnalyticsSettingModule + implements CommandModuleImplementation +{ + command = 'enable'; + aliases = 'on'; + describe = 'Enables analytics gathering and reporting for the user.'; + async run({ global }: Options): Promise { + await setAnalyticsConfig(global, true); + process.stderr.write(await getAnalyticsInfoString(this.context)); + } +} + +export class AnalyticsPromptModule + extends AnalyticsSettingModule + implements CommandModuleImplementation +{ + command = 'prompt'; + describe = 'Prompts the user to set the analytics gathering status interactively.'; + + async run({ global }: Options): Promise { + await promptAnalytics(this.context, global, true); + } +} diff --git a/packages/angular/cli/src/commands/build/cli.ts b/packages/angular/cli/src/commands/build/cli.ts new file mode 100644 index 000000000000..365420ca3734 --- /dev/null +++ b/packages/angular/cli/src/commands/build/cli.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 { join } from 'node:path'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; +import { RootCommands } from '../command-config'; + +export default class BuildCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + multiTarget = false; + command = 'build [project]'; + aliases = RootCommands['build'].aliases; + describe = + 'Compiles an Angular application or library into an output directory named dist/ at the given output path.'; + longDescriptionPath = join(__dirname, 'long-description.md'); +} diff --git a/packages/angular/cli/src/commands/build/long-description.md b/packages/angular/cli/src/commands/build/long-description.md new file mode 100644 index 000000000000..b2c14d8f23fe --- /dev/null +++ b/packages/angular/cli/src/commands/build/long-description.md @@ -0,0 +1,18 @@ +The command can be used to build a project of type "application" or "library". +When used to build a library, a different builder is invoked, and only the `ts-config`, `configuration`, `poll` and `watch` options are applied. +All other options apply only to building applications. + +The application builder uses the [esbuild](https://esbuild.github.io/) build tool, with default configuration options specified in the workspace configuration file (`angular.json`) or with a named alternative configuration. +A "development" configuration is created by default when you use the CLI to create the project, and you can use that configuration by specifying the `--configuration development`. + +The configuration options generally correspond to the command options. +You can override individual configuration defaults by specifying the corresponding options on the command line. +The command can accept option names given in dash-case. +Note that in the configuration file, you must specify names in camelCase. + +Some additional options can only be set through the configuration file, +either by direct editing or with the `ng config` command. +These include `assets`, `styles`, and `scripts` objects that provide runtime-global resources to include in the project. +Resources in CSS, such as images and fonts, are automatically written and fingerprinted at the root of the output folder. + +For further details, see [Workspace Configuration](reference/configs/workspace-config). diff --git a/packages/angular/cli/src/commands/cache/clean/cli.ts b/packages/angular/cli/src/commands/cache/clean/cli.ts new file mode 100644 index 000000000000..a115b686b7e0 --- /dev/null +++ b/packages/angular/cli/src/commands/cache/clean/cli.ts @@ -0,0 +1,37 @@ +/** + * @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 { rm } from 'node:fs/promises'; +import { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleImplementation, + CommandScope, +} from '../../../command-builder/command-module'; +import { getCacheConfig } from '../utilities'; + +export class CacheCleanModule extends CommandModule implements CommandModuleImplementation { + command = 'clean'; + describe = 'Deletes persistent disk cache from disk.'; + longDescriptionPath: string | undefined; + override scope = CommandScope.In; + + builder(localYargs: Argv): Argv { + return localYargs.strict(); + } + + run(): Promise { + const { path } = getCacheConfig(this.context.workspace); + + return rm(path, { + force: true, + recursive: true, + maxRetries: 3, + }); + } +} diff --git a/packages/angular/cli/src/commands/cache/cli.ts b/packages/angular/cli/src/commands/cache/cli.ts new file mode 100644 index 000000000000..97f3ed60f007 --- /dev/null +++ b/packages/angular/cli/src/commands/cache/cli.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 { join } from 'node:path'; +import { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleImplementation, + CommandScope, + Options, +} from '../../command-builder/command-module'; +import { + addCommandModuleToYargs, + demandCommandFailureMessage, +} from '../../command-builder/utilities/command'; +import { CacheCleanModule } from './clean/cli'; +import { CacheInfoCommandModule } from './info/cli'; +import { CacheDisableModule, CacheEnableModule } from './settings/cli'; + +export default class CacheCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'cache'; + describe = 'Configure persistent disk cache and retrieve cache statistics.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + override scope = CommandScope.In; + + builder(localYargs: Argv): Argv { + const subcommands = [ + CacheEnableModule, + CacheDisableModule, + CacheCleanModule, + CacheInfoCommandModule, + ].sort(); + + for (const module of subcommands) { + localYargs = addCommandModuleToYargs(localYargs, module, this.context); + } + + return localYargs.demandCommand(1, demandCommandFailureMessage).strict(); + } + + run(_options: Options<{}>): void {} +} diff --git a/packages/angular/cli/src/commands/cache/info/cli.ts b/packages/angular/cli/src/commands/cache/info/cli.ts new file mode 100644 index 000000000000..447d92e02c1f --- /dev/null +++ b/packages/angular/cli/src/commands/cache/info/cli.ts @@ -0,0 +1,99 @@ +/** + * @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 { tags } from '@angular-devkit/core'; +import * as fs from 'node:fs/promises'; +import { join } from 'node:path'; +import { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleImplementation, + CommandScope, +} from '../../../command-builder/command-module'; +import { isCI } from '../../../utilities/environment-options'; +import { getCacheConfig } from '../utilities'; + +export class CacheInfoCommandModule extends CommandModule implements CommandModuleImplementation { + command = 'info'; + describe = 'Prints persistent disk cache configuration and statistics in the console.'; + longDescriptionPath?: string | undefined; + override scope = CommandScope.In; + + builder(localYargs: Argv): Argv { + return localYargs.strict(); + } + + async run(): Promise { + const { path, environment, enabled } = getCacheConfig(this.context.workspace); + + this.context.logger.info(tags.stripIndents` + Enabled: ${enabled ? 'yes' : 'no'} + Environment: ${environment} + Path: ${path} + Size on disk: ${await this.getSizeOfDirectory(path)} + Effective status on current machine: ${this.effectiveEnabledStatus() ? 'enabled' : 'disabled'} + `); + } + + private async getSizeOfDirectory(path: string): Promise { + const directoriesStack = [path]; + let size = 0; + + while (directoriesStack.length) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const dirPath = directoriesStack.pop()!; + let entries: string[] = []; + + try { + entries = await fs.readdir(dirPath); + } catch {} + + for (const entry of entries) { + const entryPath = join(dirPath, entry); + const stats = await fs.stat(entryPath); + + if (stats.isDirectory()) { + directoriesStack.push(entryPath); + } + + size += stats.size; + } + } + + return this.formatSize(size); + } + + private formatSize(size: number): string { + if (size <= 0) { + return '0 bytes'; + } + + const abbreviations = ['bytes', 'kB', 'MB', 'GB']; + const index = Math.floor(Math.log(size) / Math.log(1024)); + const roundedSize = size / Math.pow(1024, index); + // bytes don't have a fraction + const fractionDigits = index === 0 ? 0 : 2; + + return `${roundedSize.toFixed(fractionDigits)} ${abbreviations[index]}`; + } + + private effectiveEnabledStatus(): boolean { + const { enabled, environment } = getCacheConfig(this.context.workspace); + + if (enabled) { + switch (environment) { + case 'ci': + return isCI; + case 'local': + return !isCI; + } + } + + return enabled; + } +} diff --git a/packages/angular/cli/src/commands/cache/long-description.md b/packages/angular/cli/src/commands/cache/long-description.md new file mode 100644 index 000000000000..3ebfec598c4e --- /dev/null +++ b/packages/angular/cli/src/commands/cache/long-description.md @@ -0,0 +1,53 @@ +Angular CLI saves a number of cachable operations on disk by default. + +When you re-run the same build, the build system restores the state of the previous build and re-uses previously performed operations, which decreases the time taken to build and test your applications and libraries. + +To amend the default cache settings, add the `cli.cache` object to your [Workspace Configuration](reference/configs/workspace-config). +The object goes under `cli.cache` at the top level of the file, outside the `projects` sections. + +```jsonc +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "cache": { + // ... + }, + }, + "projects": {}, +} +``` + +For more information, see [cache options](reference/configs/workspace-config#cache-options). + +### Cache environments + +By default, disk cache is only enabled for local environments. The value of environment can be one of the following: + +- `all` - allows disk cache on all machines. +- `local` - allows disk cache only on development machines. +- `ci` - allows disk cache only on continuous integration (CI) systems. + +To change the environment setting to `all`, run the following command: + +```bash +ng config cli.cache.environment all +``` + +For more information, see `environment` in [cache options](reference/configs/workspace-config#cache-options). + +
+ +The Angular CLI checks for the presence and value of the `CI` environment variable to determine in which environment it is running. + +
+ +### Cache path + +By default, `.angular/cache` is used as a base directory to store cache results. + +To change this path to `.cache/ng`, run the following command: + +```bash +ng config cli.cache.path ".cache/ng" +``` diff --git a/packages/angular/cli/src/commands/cache/settings/cli.ts b/packages/angular/cli/src/commands/cache/settings/cli.ts new file mode 100644 index 000000000000..9a4f654f7ac7 --- /dev/null +++ b/packages/angular/cli/src/commands/cache/settings/cli.ts @@ -0,0 +1,47 @@ +/** + * @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 { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleImplementation, + CommandScope, +} from '../../../command-builder/command-module'; +import { updateCacheConfig } from '../utilities'; + +export class CacheDisableModule extends CommandModule implements CommandModuleImplementation { + command = 'disable'; + aliases = 'off'; + describe = 'Disables persistent disk cache for all projects in the workspace.'; + longDescriptionPath: string | undefined; + override scope = CommandScope.In; + + builder(localYargs: Argv): Argv { + return localYargs; + } + + run(): Promise { + return updateCacheConfig(this.getWorkspaceOrThrow(), 'enabled', false); + } +} + +export class CacheEnableModule extends CommandModule implements CommandModuleImplementation { + command = 'enable'; + aliases = 'on'; + describe = 'Enables disk cache for all projects in the workspace.'; + longDescriptionPath: string | undefined; + override scope = CommandScope.In; + + builder(localYargs: Argv): Argv { + return localYargs; + } + + run(): Promise { + return updateCacheConfig(this.getWorkspaceOrThrow(), 'enabled', true); + } +} diff --git a/packages/angular/cli/src/commands/cache/utilities.ts b/packages/angular/cli/src/commands/cache/utilities.ts new file mode 100644 index 000000000000..84e22314763a --- /dev/null +++ b/packages/angular/cli/src/commands/cache/utilities.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 { isJsonObject } from '@angular-devkit/core'; +import { resolve } from 'node:path'; +import { Cache, Environment } from '../../../lib/config/workspace-schema'; +import { AngularWorkspace } from '../../utilities/config'; + +export function updateCacheConfig( + workspace: AngularWorkspace, + key: K, + value: Cache[K], +): Promise { + const cli = (workspace.extensions['cli'] ??= {}) as Record>; + const cache = (cli['cache'] ??= {}); + cache[key] = value; + + return workspace.save(); +} + +export function getCacheConfig(workspace: AngularWorkspace | undefined): Required { + if (!workspace) { + throw new Error(`Cannot retrieve cache configuration as workspace is not defined.`); + } + + const defaultSettings: Required = { + path: resolve(workspace.basePath, '.angular/cache'), + environment: Environment.Local, + enabled: true, + }; + + const cliSetting = workspace.extensions['cli']; + if (!cliSetting || !isJsonObject(cliSetting)) { + return defaultSettings; + } + + const cacheSettings = cliSetting['cache']; + if (!isJsonObject(cacheSettings)) { + return defaultSettings; + } + + const { + path = defaultSettings.path, + environment = defaultSettings.environment, + enabled = defaultSettings.enabled, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } = cacheSettings as Record; + + return { + path: resolve(workspace.basePath, path), + environment, + enabled, + }; +} diff --git a/packages/angular/cli/src/commands/command-config.ts b/packages/angular/cli/src/commands/command-config.ts new file mode 100644 index 000000000000..cd048cbb2240 --- /dev/null +++ b/packages/angular/cli/src/commands/command-config.ts @@ -0,0 +1,113 @@ +/** + * @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 { CommandModuleConstructor } from '../command-builder/utilities/command'; + +export type CommandNames = + | 'add' + | 'analytics' + | 'build' + | 'cache' + | 'completion' + | 'config' + | 'deploy' + | 'e2e' + | 'extract-i18n' + | 'generate' + | 'lint' + | 'make-this-awesome' + | 'new' + | 'run' + | 'serve' + | 'test' + | 'update' + | 'version'; + +export interface CommandConfig { + aliases?: string[]; + factory: () => Promise<{ default: CommandModuleConstructor }>; +} + +export const RootCommands: Record< + /* Command */ CommandNames & string, + /* Command Config */ CommandConfig +> = { + 'add': { + factory: () => import('./add/cli'), + }, + 'analytics': { + factory: () => import('./analytics/cli'), + }, + 'build': { + factory: () => import('./build/cli'), + aliases: ['b'], + }, + 'cache': { + factory: () => import('./cache/cli'), + }, + 'completion': { + factory: () => import('./completion/cli'), + }, + 'config': { + factory: () => import('./config/cli'), + }, + 'deploy': { + factory: () => import('./deploy/cli'), + }, + + 'e2e': { + factory: () => import('./e2e/cli'), + aliases: ['e'], + }, + 'extract-i18n': { + factory: () => import('./extract-i18n/cli'), + }, + 'generate': { + factory: () => import('./generate/cli'), + aliases: ['g'], + }, + 'lint': { + factory: () => import('./lint/cli'), + }, + 'make-this-awesome': { + factory: () => import('./make-this-awesome/cli'), + }, + 'new': { + factory: () => import('./new/cli'), + aliases: ['n'], + }, + 'run': { + factory: () => import('./run/cli'), + }, + 'serve': { + factory: () => import('./serve/cli'), + aliases: ['dev', 's'], + }, + 'test': { + factory: () => import('./test/cli'), + aliases: ['t'], + }, + 'update': { + factory: () => import('./update/cli'), + }, + 'version': { + factory: () => import('./version/cli'), + aliases: ['v'], + }, +}; + +export const RootCommandsAliases = Object.values(RootCommands).reduce( + (prev, current) => { + current.aliases?.forEach((alias) => { + prev[alias] = current; + }); + + return prev; + }, + {} as Record, +); diff --git a/packages/angular/cli/src/commands/completion/cli.ts b/packages/angular/cli/src/commands/completion/cli.ts new file mode 100644 index 000000000000..ebbf4ffc992f --- /dev/null +++ b/packages/angular/cli/src/commands/completion/cli.ts @@ -0,0 +1,74 @@ +/** + * @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 { join } from 'node:path'; +import yargs, { Argv } from 'yargs'; +import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; +import { addCommandModuleToYargs } from '../../command-builder/utilities/command'; +import { colors } from '../../utilities/color'; +import { hasGlobalCliInstall, initializeAutocomplete } from '../../utilities/completion'; +import { assertIsError } from '../../utilities/error'; + +export default class CompletionCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'completion'; + describe = 'Set up Angular CLI autocompletion for your terminal.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + + builder(localYargs: Argv): Argv { + return addCommandModuleToYargs(localYargs, CompletionScriptCommandModule, this.context); + } + + async run(): Promise { + let rcFile: string; + try { + rcFile = await initializeAutocomplete(); + } catch (err) { + assertIsError(err); + this.context.logger.error(err.message); + + return 1; + } + + this.context.logger.info( + ` +Appended \`source <(ng completion script)\` to \`${rcFile}\`. Restart your terminal or run the following to autocomplete \`ng\` commands: + + ${colors.yellow('source <(ng completion script)')} + `.trim(), + ); + + if ((await hasGlobalCliInstall()) === false) { + this.context.logger.warn( + 'Setup completed successfully, but there does not seem to be a global install of the' + + ' Angular CLI. For autocompletion to work, the CLI will need to be on your `$PATH`, which' + + ' is typically done with the `-g` flag in `npm install -g @angular/cli`.' + + '\n\n' + + 'For more information, see https://angular.dev/cli/completion#global-install', + ); + } + + return 0; + } +} + +class CompletionScriptCommandModule extends CommandModule implements CommandModuleImplementation { + command = 'script'; + describe = 'Generate a bash and zsh real-time type-ahead autocompletion script.'; + longDescriptionPath = undefined; + + builder(localYargs: Argv): Argv { + return localYargs; + } + + run(): void { + yargs.showCompletionScript(); + } +} diff --git a/packages/angular/cli/src/commands/completion/long-description.md b/packages/angular/cli/src/commands/completion/long-description.md new file mode 100644 index 000000000000..b75803ac9cb0 --- /dev/null +++ b/packages/angular/cli/src/commands/completion/long-description.md @@ -0,0 +1,67 @@ +Setting up autocompletion configures your terminal, so pressing the `` key while in the middle +of typing will display various commands and options available to you. This makes it very easy to +discover and use CLI commands without lots of memorization. + +![A demo of Angular CLI autocompletion in a terminal. The user types several partial `ng` commands, +using autocompletion to finish several arguments and list contextual options. +](assets/images/guide/cli/completion.gif) + +## Automated setup + +The CLI should prompt and ask to set up autocompletion for you the first time you use it (v14+). +Simply answer "Yes" and the CLI will take care of the rest. + +``` +$ ng serve +? Would you like to enable autocompletion? This will set up your terminal so pressing TAB while typing Angular CLI commands will show possible options and autocomplete arguments. (Enabling autocompletion will modify configuration files in your home directory.) Yes +Appended `source <(ng completion script)` to `/home/my-username/.bashrc`. Restart your terminal or run: + +source <(ng completion script) + +to autocomplete `ng` commands. + +# Serve output... +``` + +If you already refused the prompt, it won't ask again. But you can run `ng completion` to +do the same thing automatically. + +This modifies your terminal environment to load Angular CLI autocompletion, but can't update your +current terminal session. Either restart it or run `source <(ng completion script)` directly to +enable autocompletion in your current session. + +Test it out by typing `ng ser` and it should autocomplete to `ng serve`. Ambiguous arguments +will show all possible options and their documentation, such as `ng generate `. + +## Manual setup + +Some users may have highly customized terminal setups, possibly with configuration files checked +into source control with an opinionated structure. `ng completion` only ever appends Angular's setup +to an existing configuration file for your current shell, or creates one if none exists. If you want +more control over exactly where this configuration lives, you can manually set it up by having your +shell run at startup: + +```bash +source <(ng completion script) +``` + +This is equivalent to what `ng completion` will automatically set up, and gives power users more +flexibility in their environments when desired. + +## Platform support + +Angular CLI supports autocompletion for the Bash and Zsh shells on MacOS and Linux operating +systems. On Windows, Git Bash and [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/) +using Bash or Zsh are supported. + +## Global install + +Autocompletion works by configuring your terminal to invoke the Angular CLI on startup to load the +setup script. This means the terminal must be able to find and execute the Angular CLI, typically +through a global install that places the binary on the user's `$PATH`. If you get +`command not found: ng`, make sure the CLI is installed globally which you can do with the `-g` +flag: + +```bash +npm install -g @angular/cli +``` diff --git a/packages/angular/cli/src/commands/config/cli.ts b/packages/angular/cli/src/commands/config/cli.ts new file mode 100644 index 000000000000..06b253b9a42d --- /dev/null +++ b/packages/angular/cli/src/commands/config/cli.ts @@ -0,0 +1,192 @@ +/** + * @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 { JsonValue } from '@angular-devkit/core'; +import { randomUUID } from 'node:crypto'; +import { join } from 'node:path'; +import { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleError, + CommandModuleImplementation, + Options, +} from '../../command-builder/command-module'; +import { getWorkspaceRaw, validateWorkspace } from '../../utilities/config'; +import { JSONFile, parseJson } from '../../utilities/json-file'; + +interface ConfigCommandArgs { + 'json-path'?: string; + value?: string; + global?: boolean; +} + +export default class ConfigCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'config [json-path] [value]'; + describe = + 'Retrieves or sets Angular configuration values in the angular.json file for the workspace.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + + builder(localYargs: Argv): Argv { + return localYargs + .positional('json-path', { + description: + `The configuration key to set or query, in JSON path format. ` + + `For example: "a[3].foo.bar[2]". If no new value is provided, returns the current value of this key.`, + type: 'string', + }) + .positional('value', { + description: 'If provided, a new value for the given configuration key.', + type: 'string', + }) + .option('global', { + description: `Access the global configuration in the caller's home directory.`, + alias: ['g'], + type: 'boolean', + default: false, + }) + .strict(); + } + + async run(options: Options): Promise { + const level = options.global ? 'global' : 'local'; + const [config] = await getWorkspaceRaw(level); + + if (options.value == undefined) { + if (!config) { + this.context.logger.error('No config found.'); + + return 1; + } + + return this.get(config, options); + } else { + return this.set(options); + } + } + + private get(jsonFile: JSONFile, options: Options): number { + const { logger } = this.context; + + const value = options.jsonPath + ? jsonFile.get(parseJsonPath(options.jsonPath)) + : jsonFile.content; + + if (value === undefined) { + logger.error('Value cannot be found.'); + + return 1; + } else if (typeof value === 'string') { + logger.info(value); + } else { + logger.info(JSON.stringify(value, null, 2)); + } + + return 0; + } + + private async set(options: Options): Promise { + if (!options.jsonPath?.trim()) { + throw new CommandModuleError('Invalid Path.'); + } + + const [config, configPath] = await getWorkspaceRaw(options.global ? 'global' : 'local'); + const { logger } = this.context; + + if (!config || !configPath) { + throw new CommandModuleError('Confguration file cannot be found.'); + } + + const normalizeUUIDValue = (v: string | undefined) => (v === '' ? randomUUID() : `${v}`); + + const value = + options.jsonPath === 'cli.analyticsSharing.uuid' + ? normalizeUUIDValue(options.value) + : options.value; + + const modified = config.modify(parseJsonPath(options.jsonPath), normalizeValue(value)); + + if (!modified) { + logger.error('Value cannot be found.'); + + return 1; + } + + await validateWorkspace(parseJson(config.content), options.global ?? false); + + config.save(); + + return 0; + } +} + +/** + * Splits a JSON path string into fragments. Fragments can be used to get the value referenced + * by the path. For example, a path of "a[3].foo.bar[2]" would give you a fragment array of + * ["a", 3, "foo", "bar", 2]. + * @param path The JSON string to parse. + * @returns {(string|number)[]} The fragments for the string. + * @private + */ +function parseJsonPath(path: string): (string | number)[] { + const fragments = (path || '').split(/\./g); + const result: (string | number)[] = []; + + while (fragments.length > 0) { + const fragment = fragments.shift(); + if (fragment == undefined) { + break; + } + + const match = fragment.match(/([^[]+)((\[.*\])*)/); + if (!match) { + throw new CommandModuleError('Invalid JSON path.'); + } + + result.push(match[1]); + if (match[2]) { + const indices = match[2] + .slice(1, -1) + .split('][') + .map((x) => (/^\d$/.test(x) ? +x : x.replace(/"|'/g, ''))); + result.push(...indices); + } + } + + return result.filter((fragment) => fragment != null); +} + +function normalizeValue(value: string | undefined | boolean | number): JsonValue | undefined { + const valueString = `${value}`.trim(); + switch (valueString) { + case 'true': + return true; + case 'false': + return false; + case 'null': + return null; + case 'undefined': + return undefined; + } + + if (isFinite(+valueString)) { + return +valueString; + } + + try { + // We use `JSON.parse` instead of `parseJson` because the latter will parse UUIDs + // and convert them into a numberic entities. + // Example: 73b61974-182c-48e4-b4c6-30ddf08c5c98 -> 73. + // These values should never contain comments, therefore using `JSON.parse` is safe. + return JSON.parse(valueString) as JsonValue; + } catch { + return value; + } +} diff --git a/packages/angular/cli/src/commands/config/long-description.md b/packages/angular/cli/src/commands/config/long-description.md new file mode 100644 index 000000000000..db32cb294152 --- /dev/null +++ b/packages/angular/cli/src/commands/config/long-description.md @@ -0,0 +1,13 @@ +A workspace has a single CLI configuration file, `angular.json`, at the top level. +The `projects` object contains a configuration object for each project in the workspace. + +You can edit the configuration directly in a code editor, +or indirectly on the command line using this command. + +The configurable property names match command option names, +except that in the configuration file, all names must use camelCase, +while on the command line options can be given dash-case. + +For further details, see [Workspace Configuration](reference/configs/workspace-config). + +For configuration of CLI usage analytics, see [ng analytics](cli/analytics). diff --git a/packages/angular/cli/src/commands/deploy/cli.ts b/packages/angular/cli/src/commands/deploy/cli.ts new file mode 100644 index 000000000000..947dc90af2d4 --- /dev/null +++ b/packages/angular/cli/src/commands/deploy/cli.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 { join } from 'node:path'; +import { MissingTargetChoice } from '../../command-builder/architect-base-command-module'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; + +export default class DeployCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + // The below choices should be kept in sync with the list in https://angular.dev/tools/cli/deployment + override missingTargetChoices: MissingTargetChoice[] = [ + { + name: 'Amazon S3', + value: '@jefiozie/ngx-aws-deploy', + }, + { + name: 'Firebase', + value: '@angular/fire', + }, + { + name: 'Netlify', + value: '@netlify-builder/deploy', + }, + { + name: 'GitHub Pages', + value: 'angular-cli-ghpages', + }, + ]; + + multiTarget = false; + command = 'deploy [project]'; + longDescriptionPath = join(__dirname, 'long-description.md'); + describe = + 'Invokes the deploy builder for a specified project or for the default project in the workspace.'; +} diff --git a/packages/angular/cli/src/commands/deploy/long-description.md b/packages/angular/cli/src/commands/deploy/long-description.md new file mode 100644 index 000000000000..0436390680a4 --- /dev/null +++ b/packages/angular/cli/src/commands/deploy/long-description.md @@ -0,0 +1,22 @@ +The command takes an optional project name, as specified in the `projects` section of the `angular.json` workspace configuration file. +When a project name is not supplied, executes the `deploy` builder for the default project. + +To use the `ng deploy` command, use `ng add` to add a package that implements deployment capabilities to your favorite platform. +Adding the package automatically updates your workspace configuration, adding a deployment +[CLI builder](tools/cli/cli-builder). +For example: + +```json +"projects": { + "my-project": { + ... + "architect": { + ... + "deploy": { + "builder": "@angular/fire:deploy", + "options": {} + } + } + } +} +``` diff --git a/packages/angular/cli/src/commands/e2e/cli.ts b/packages/angular/cli/src/commands/e2e/cli.ts new file mode 100644 index 000000000000..85d9aab173a0 --- /dev/null +++ b/packages/angular/cli/src/commands/e2e/cli.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 { MissingTargetChoice } from '../../command-builder/architect-base-command-module'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; +import { RootCommands } from '../command-config'; + +export default class E2eCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + override missingTargetChoices: MissingTargetChoice[] = [ + { + name: 'Playwright', + value: 'playwright-ng-schematics', + }, + { + name: 'Cypress', + value: '@cypress/schematic', + }, + { + name: 'Nightwatch', + value: '@nightwatch/schematics', + }, + { + name: 'WebdriverIO', + value: '@wdio/schematics', + }, + { + name: 'Puppeteer', + value: '@puppeteer/ng-schematics', + }, + ]; + + multiTarget = true; + command = 'e2e [project]'; + aliases = RootCommands['e2e'].aliases; + describe = 'Builds and serves an Angular application, then runs end-to-end tests.'; + longDescriptionPath?: string; +} diff --git a/packages/angular/cli/src/commands/extract-i18n/cli.ts b/packages/angular/cli/src/commands/extract-i18n/cli.ts new file mode 100644 index 000000000000..4f3dea2d8e7e --- /dev/null +++ b/packages/angular/cli/src/commands/extract-i18n/cli.ts @@ -0,0 +1,57 @@ +/** + * @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 { workspaces } from '@angular-devkit/core'; +import { createRequire } from 'node:module'; +import { join } from 'node:path'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; + +export default class ExtractI18nCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + multiTarget = false; + command = 'extract-i18n [project]'; + describe = 'Extracts i18n messages from source code.'; + longDescriptionPath?: string | undefined; + + override async findDefaultBuilderName( + project: workspaces.ProjectDefinition, + ): Promise { + // Only application type projects have a default i18n extraction target + if (project.extensions['projectType'] !== 'application') { + return; + } + + const buildTarget = project.targets.get('build'); + if (!buildTarget) { + // No default if there is no build target + return; + } + + // Provide a default based on the defined builder for the 'build' target + switch (buildTarget.builder) { + case '@angular-devkit/build-angular:application': + case '@angular-devkit/build-angular:browser-esbuild': + case '@angular-devkit/build-angular:browser': + return '@angular-devkit/build-angular:extract-i18n'; + case '@angular/build:application': + return '@angular/build:extract-i18n'; + } + + // For other builders, check for `@angular-devkit/build-angular` and use if found. + // This package is safer to use since it supports both application builder types. + try { + const projectRequire = createRequire(join(this.context.root, project.root) + '/'); + projectRequire.resolve('@angular-devkit/build-angular'); + + return '@angular-devkit/build-angular:extract-i18n'; + } catch {} + } +} diff --git a/packages/angular/cli/src/commands/generate/cli.ts b/packages/angular/cli/src/commands/generate/cli.ts new file mode 100644 index 000000000000..4be29c3eaea0 --- /dev/null +++ b/packages/angular/cli/src/commands/generate/cli.ts @@ -0,0 +1,283 @@ +/** + * @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 { strings } from '@angular-devkit/core'; +import { Collection } from '@angular-devkit/schematics'; +import { + FileSystemCollectionDescription, + FileSystemSchematicDescription, +} from '@angular-devkit/schematics/tools'; +import { ArgumentsCamelCase, Argv } from 'yargs'; +import { + CommandModuleError, + CommandModuleImplementation, + Options, + OtherOptions, +} from '../../command-builder/command-module'; +import { + SchematicsCommandArgs, + SchematicsCommandModule, +} from '../../command-builder/schematics-command-module'; +import { demandCommandFailureMessage } from '../../command-builder/utilities/command'; +import { Option } from '../../command-builder/utilities/json-schema'; +import { RootCommands } from '../command-config'; + +interface GenerateCommandArgs extends SchematicsCommandArgs { + schematic?: string; +} + +export default class GenerateCommandModule + extends SchematicsCommandModule + implements CommandModuleImplementation +{ + command = 'generate'; + aliases = RootCommands['generate'].aliases; + describe = 'Generates and/or modifies files based on a schematic.'; + longDescriptionPath?: string | undefined; + + override async builder(argv: Argv): Promise> { + let localYargs = (await super.builder(argv)).command({ + command: '$0 ', + describe: 'Run the provided schematic.', + builder: (localYargs) => + localYargs + .positional('schematic', { + describe: 'The [collection:schematic] to run.', + type: 'string', + demandOption: true, + }) + .strict(), + handler: (options) => this.handler(options as ArgumentsCamelCase), + }); + + for (const [schematicName, collectionName] of await this.getSchematicsToRegister()) { + const workflow = this.getOrCreateWorkflowForBuilder(collectionName); + const collection = workflow.engine.createCollection(collectionName); + + const { + description: { + schemaJson, + aliases: schematicAliases, + hidden: schematicHidden, + description: schematicDescription, + }, + } = collection.createSchematic(schematicName, true); + + if (!schemaJson) { + continue; + } + + const { + 'x-deprecated': xDeprecated, + description = schematicDescription, + hidden = schematicHidden, + } = schemaJson; + const options = await this.getSchematicOptions(collection, schematicName, workflow); + + localYargs = localYargs.command({ + command: await this.generateCommandString(collectionName, schematicName, options), + // When 'describe' is set to false, it results in a hidden command. + describe: hidden === true ? false : typeof description === 'string' ? description : '', + deprecated: xDeprecated === true || typeof xDeprecated === 'string' ? xDeprecated : false, + aliases: Array.isArray(schematicAliases) + ? await this.generateCommandAliasesStrings(collectionName, schematicAliases) + : undefined, + builder: (localYargs) => this.addSchemaOptionsToCommand(localYargs, options).strict(), + handler: (options) => + this.handler({ + ...options, + schematic: `${collectionName}:${schematicName}`, + } as ArgumentsCamelCase< + SchematicsCommandArgs & { + schematic: string; + } + >), + }); + } + + return localYargs.demandCommand(1, demandCommandFailureMessage); + } + + async run(options: Options & OtherOptions): Promise { + const { dryRun, schematic, defaults, force, interactive, ...schematicOptions } = options; + + const [collectionName, schematicName] = this.parseSchematicInfo(schematic); + + if (!collectionName || !schematicName) { + throw new CommandModuleError('A collection and schematic is required during execution.'); + } + + return this.runSchematic({ + collectionName, + schematicName, + schematicOptions, + executionOptions: { + dryRun, + defaults, + force, + interactive, + }, + }); + } + + private async getCollectionNames(): Promise { + const [collectionName] = this.parseSchematicInfo( + // positional = [generate, component] or [generate] + this.context.args.positional[1], + ); + + return collectionName ? [collectionName] : [...(await this.getSchematicCollections())]; + } + + private async shouldAddCollectionNameAsPartOfCommand(): Promise { + const [collectionNameFromArgs] = this.parseSchematicInfo( + // positional = [generate, component] or [generate] + this.context.args.positional[1], + ); + + const schematicCollectionsFromConfig = await this.getSchematicCollections(); + const collectionNames = await this.getCollectionNames(); + + // Only add the collection name as part of the command when it's not a known + // schematics collection or when it has been provided via the CLI. + // Ex:`ng generate @schematics/angular:c` + return ( + !!collectionNameFromArgs || + !collectionNames.some((c) => schematicCollectionsFromConfig.has(c)) + ); + } + + /** + * Generate an aliases string array to be passed to the command builder. + * + * @example `[component]` or `[@schematics/angular:component]`. + */ + private async generateCommandAliasesStrings( + collectionName: string, + schematicAliases: string[], + ): Promise { + // Only add the collection name as part of the command when it's not a known + // schematics collection or when it has been provided via the CLI. + // Ex:`ng generate @schematics/angular:c` + return (await this.shouldAddCollectionNameAsPartOfCommand()) + ? schematicAliases.map((alias) => `${collectionName}:${alias}`) + : schematicAliases; + } + + /** + * Generate a command string to be passed to the command builder. + * + * @example `component [name]` or `@schematics/angular:component [name]`. + */ + private async generateCommandString( + collectionName: string, + schematicName: string, + options: Option[], + ): Promise { + const dasherizedSchematicName = strings.dasherize(schematicName); + + // Only add the collection name as part of the command when it's not a known + // schematics collection or when it has been provided via the CLI. + // Ex:`ng generate @schematics/angular:component` + const commandName = (await this.shouldAddCollectionNameAsPartOfCommand()) + ? collectionName + ':' + dasherizedSchematicName + : dasherizedSchematicName; + + const positionalArgs = options + .filter((o) => o.positional !== undefined) + .map((o) => { + const label = `${strings.dasherize(o.name)}${o.type === 'array' ? ' ..' : ''}`; + + return o.required ? `<${label}>` : `[${label}]`; + }) + .join(' '); + + return `${commandName}${positionalArgs ? ' ' + positionalArgs : ''}`; + } + + /** + * Get schematics that can to be registered as subcommands. + */ + private async *getSchematics(): AsyncGenerator<{ + schematicName: string; + schematicAliases?: Set; + collectionName: string; + }> { + const seenNames = new Set(); + for (const collectionName of await this.getCollectionNames()) { + const workflow = this.getOrCreateWorkflowForBuilder(collectionName); + const collection = workflow.engine.createCollection(collectionName); + + for (const schematicName of collection.listSchematicNames(true /** includeHidden */)) { + // If a schematic with this same name is already registered skip. + if (!seenNames.has(schematicName)) { + seenNames.add(schematicName); + + yield { + schematicName, + collectionName, + schematicAliases: this.listSchematicAliases(collection, schematicName), + }; + } + } + } + } + + private listSchematicAliases( + collection: Collection, + schematicName: string, + ): Set | undefined { + const description = collection.description.schematics[schematicName]; + if (description) { + return description.aliases && new Set(description.aliases); + } + + // Extended collections + if (collection.baseDescriptions) { + for (const base of collection.baseDescriptions) { + const description = base.schematics[schematicName]; + if (description) { + return description.aliases && new Set(description.aliases); + } + } + } + + return undefined; + } + + /** + * Get schematics that should to be registered as subcommands. + * + * @returns a sorted list of schematic that needs to be registered as subcommands. + */ + private async getSchematicsToRegister(): Promise< + [schematicName: string, collectionName: string][] + > { + const schematicsToRegister: [schematicName: string, collectionName: string][] = []; + const [, schematicNameFromArgs] = this.parseSchematicInfo( + // positional = [generate, component] or [generate] + this.context.args.positional[1], + ); + + for await (const { schematicName, collectionName, schematicAliases } of this.getSchematics()) { + if ( + schematicNameFromArgs && + (schematicName === schematicNameFromArgs || schematicAliases?.has(schematicNameFromArgs)) + ) { + return [[schematicName, collectionName]]; + } + + schematicsToRegister.push([schematicName, collectionName]); + } + + // Didn't find the schematic or no schematic name was provided Ex: `ng generate --help`. + return schematicsToRegister.sort(([nameA], [nameB]) => + nameA.localeCompare(nameB, undefined, { sensitivity: 'accent' }), + ); + } +} diff --git a/packages/angular/cli/src/commands/lint/cli.ts b/packages/angular/cli/src/commands/lint/cli.ts new file mode 100644 index 000000000000..9510dd7afe53 --- /dev/null +++ b/packages/angular/cli/src/commands/lint/cli.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 { join } from 'node:path'; +import { MissingTargetChoice } from '../../command-builder/architect-base-command-module'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; + +export default class LintCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + override missingTargetChoices: MissingTargetChoice[] = [ + { + name: 'ESLint', + value: 'angular-eslint', + }, + ]; + + multiTarget = true; + command = 'lint [project]'; + longDescriptionPath = join(__dirname, 'long-description.md'); + describe = 'Runs linting tools on Angular application code in a given project folder.'; +} diff --git a/packages/angular/cli/src/commands/lint/long-description.md b/packages/angular/cli/src/commands/lint/long-description.md new file mode 100644 index 000000000000..5e5fa3da951c --- /dev/null +++ b/packages/angular/cli/src/commands/lint/long-description.md @@ -0,0 +1,20 @@ +The command takes an optional project name, as specified in the `projects` section of the `angular.json` workspace configuration file. +When a project name is not supplied, executes the `lint` builder for all projects. + +To use the `ng lint` command, use `ng add` to add a package that implements linting capabilities. Adding the package automatically updates your workspace configuration, adding a lint [CLI builder](tools/cli/cli-builder). +For example: + +```json +"projects": { + "my-project": { + ... + "architect": { + ... + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": {} + } + } + } +} +``` diff --git a/packages/angular/cli/src/commands/make-this-awesome/cli.ts b/packages/angular/cli/src/commands/make-this-awesome/cli.ts new file mode 100644 index 000000000000..6a17c5614b94 --- /dev/null +++ b/packages/angular/cli/src/commands/make-this-awesome/cli.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 { Argv } from 'yargs'; +import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; +import { colors } from '../../utilities/color'; + +export default class AwesomeCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'make-this-awesome'; + describe = false as const; + deprecated = false; + longDescriptionPath?: string | undefined; + + builder(localYargs: Argv): Argv { + return localYargs; + } + + run(): void { + const pickOne = (of: string[]) => of[Math.floor(Math.random() * of.length)]; + + const phrase = pickOne([ + `You're on it, there's nothing for me to do!`, + `Let's take a look... nope, it's all good!`, + `You're doing fine.`, + `You're already doing great.`, + `Nothing to do; already awesome. Exiting.`, + `Error 418: As Awesome As Can Get.`, + `I spy with my little eye a great developer!`, + `Noop... already awesome.`, + ]); + + this.context.logger.info(colors.green(phrase)); + } +} diff --git a/packages/angular/cli/src/commands/new/cli.ts b/packages/angular/cli/src/commands/new/cli.ts new file mode 100644 index 000000000000..9163708726b6 --- /dev/null +++ b/packages/angular/cli/src/commands/new/cli.ts @@ -0,0 +1,104 @@ +/** + * @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 { join } from 'node:path'; +import { Argv } from 'yargs'; +import { + CommandModuleImplementation, + CommandScope, + Options, + OtherOptions, +} from '../../command-builder/command-module'; +import { + DEFAULT_SCHEMATICS_COLLECTION, + SchematicsCommandArgs, + SchematicsCommandModule, +} from '../../command-builder/schematics-command-module'; +import { VERSION } from '../../utilities/version'; +import { RootCommands } from '../command-config'; + +interface NewCommandArgs extends SchematicsCommandArgs { + collection?: string; +} + +export default class NewCommandModule + extends SchematicsCommandModule + implements CommandModuleImplementation +{ + private readonly schematicName = 'ng-new'; + override scope = CommandScope.Out; + protected override allowPrivateSchematics = true; + + command = 'new [name]'; + aliases = RootCommands['new'].aliases; + describe = 'Creates a new Angular workspace.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + + override async builder(argv: Argv): Promise> { + const localYargs = (await super.builder(argv)).option('collection', { + alias: 'c', + describe: 'A collection of schematics to use in generating the initial application.', + type: 'string', + }); + + const { + options: { collection: collectionNameFromArgs }, + } = this.context.args; + + const collectionName = + typeof collectionNameFromArgs === 'string' + ? collectionNameFromArgs + : await this.getCollectionFromConfig(); + + const workflow = this.getOrCreateWorkflowForBuilder(collectionName); + const collection = workflow.engine.createCollection(collectionName); + const options = await this.getSchematicOptions(collection, this.schematicName, workflow); + + return this.addSchemaOptionsToCommand(localYargs, options); + } + + async run(options: Options & OtherOptions): Promise { + // Register the version of the CLI in the registry. + const collectionName = options.collection ?? (await this.getCollectionFromConfig()); + const { dryRun, force, interactive, defaults, collection, ...schematicOptions } = options; + const workflow = await this.getOrCreateWorkflowForExecution(collectionName, { + dryRun, + force, + interactive, + defaults, + }); + workflow.registry.addSmartDefaultProvider('ng-cli-version', () => VERSION.full); + + return this.runSchematic({ + collectionName, + schematicName: this.schematicName, + schematicOptions, + executionOptions: { + dryRun, + force, + interactive, + defaults, + }, + }); + } + + /** Find a collection from config that has an `ng-new` schematic. */ + private async getCollectionFromConfig(): Promise { + for (const collectionName of await this.getSchematicCollections()) { + const workflow = this.getOrCreateWorkflowForBuilder(collectionName); + const collection = workflow.engine.createCollection(collectionName); + const schematicsInCollection = collection.description.schematics; + + if (Object.keys(schematicsInCollection).includes(this.schematicName)) { + return collectionName; + } + } + + return DEFAULT_SCHEMATICS_COLLECTION; + } +} diff --git a/packages/angular/cli/src/commands/new/long-description.md b/packages/angular/cli/src/commands/new/long-description.md new file mode 100644 index 000000000000..1166f974887a --- /dev/null +++ b/packages/angular/cli/src/commands/new/long-description.md @@ -0,0 +1,15 @@ +Creates and initializes a new Angular application that is the default project for a new workspace. + +Provides interactive prompts for optional configuration, such as adding routing support. +All prompts can safely be allowed to default. + +- The new workspace folder is given the specified project name, and contains configuration files at the top level. + +- By default, the files for a new initial application (with the same name as the workspace) are placed in the `src/` subfolder. +- The new application's configuration appears in the `projects` section of the `angular.json` workspace configuration file, under its project name. + +- Subsequent applications that you generate in the workspace reside in the `projects/` subfolder. + +If you plan to have multiple applications in the workspace, you can create an empty workspace by using the `--no-create-application` option. +You can then use `ng generate application` to create an initial application. +This allows a workspace name different from the initial app name, and ensures that all applications reside in the `/projects` subfolder, matching the structure of the configuration file. diff --git a/packages/angular/cli/src/commands/run/cli.ts b/packages/angular/cli/src/commands/run/cli.ts new file mode 100644 index 000000000000..aa12cd0158f7 --- /dev/null +++ b/packages/angular/cli/src/commands/run/cli.ts @@ -0,0 +1,126 @@ +/** + * @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 { Target } from '@angular-devkit/architect'; +import { join } from 'node:path'; +import { Argv } from 'yargs'; +import { ArchitectBaseCommandModule } from '../../command-builder/architect-base-command-module'; +import { + CommandModuleError, + CommandModuleImplementation, + CommandScope, + Options, + OtherOptions, +} from '../../command-builder/command-module'; + +export interface RunCommandArgs { + target: string; +} + +export default class RunCommandModule + extends ArchitectBaseCommandModule + implements CommandModuleImplementation +{ + override scope = CommandScope.In; + + command = 'run '; + describe = + 'Runs an Architect target with an optional custom builder configuration defined in your project.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + + async builder(argv: Argv): Promise> { + const { jsonHelp, getYargsCompletions, help } = this.context.args.options; + + const localYargs: Argv = argv + .positional('target', { + describe: + 'The Architect target to run provided in the following format `project:target[:configuration]`.', + type: 'string', + demandOption: true, + // Show only in when using --help and auto completion because otherwise comma seperated configuration values will be invalid. + // Also, hide choices from JSON help so that we don't display them in AIO. + choices: (getYargsCompletions || help) && !jsonHelp ? this.getTargetChoices() : undefined, + }) + .middleware((args) => { + // TODO: remove in version 15. + const { configuration, target } = args; + if (typeof configuration === 'string' && target) { + const targetWithConfig = target.split(':', 2); + targetWithConfig.push(configuration); + + throw new CommandModuleError( + 'Unknown argument: configuration.\n' + + `Provide the configuration as part of the target 'ng run ${targetWithConfig.join( + ':', + )}'.`, + ); + } + }, true) + .strict(); + + const target = this.makeTargetSpecifier(); + if (!target) { + return localYargs; + } + + const schemaOptions = await this.getArchitectTargetOptions(target); + + return this.addSchemaOptionsToCommand(localYargs, schemaOptions); + } + + async run(options: Options & OtherOptions): Promise { + const target = this.makeTargetSpecifier(options); + const { target: _target, ...extraOptions } = options; + + if (!target) { + throw new CommandModuleError('Cannot determine project or target.'); + } + + return this.runSingleTarget(target, extraOptions); + } + + protected makeTargetSpecifier(options?: Options): Target | undefined { + const architectTarget = options?.target ?? this.context.args.positional[1]; + if (!architectTarget) { + return undefined; + } + + const [project = '', target = '', configuration] = architectTarget.split(':'); + + return { + project, + target, + configuration, + }; + } + + /** @returns a sorted list of target specifiers to be used for auto completion. */ + private getTargetChoices(): string[] | undefined { + if (!this.context.workspace) { + return; + } + + const targets = []; + for (const [projectName, project] of this.context.workspace.projects) { + for (const [targetName, target] of project.targets) { + const currentTarget = `${projectName}:${targetName}`; + targets.push(currentTarget); + + if (!target.configurations) { + continue; + } + + for (const configName of Object.keys(target.configurations)) { + targets.push(`${currentTarget}:${configName}`); + } + } + } + + return targets.sort(); + } +} diff --git a/packages/angular/cli/src/commands/run/long-description.md b/packages/angular/cli/src/commands/run/long-description.md new file mode 100644 index 000000000000..e74f8756679d --- /dev/null +++ b/packages/angular/cli/src/commands/run/long-description.md @@ -0,0 +1,10 @@ +Architect is the tool that the CLI uses to perform complex tasks such as compilation, according to provided configurations. +The CLI commands run Architect targets such as `build`, `serve`, `test`, and `lint`. +Each named target has a default configuration, specified by an `options` object, +and an optional set of named alternate configurations in the `configurations` object. + +For example, the `serve` target for a newly generated app has a predefined +alternate configuration named `production`. + +You can define new targets and their configuration options in the `architect` section +of the `angular.json` file which you can run them from the command line using the `ng run` command. diff --git a/packages/angular/cli/src/commands/serve/cli.ts b/packages/angular/cli/src/commands/serve/cli.ts new file mode 100644 index 000000000000..3b38fa122acd --- /dev/null +++ b/packages/angular/cli/src/commands/serve/cli.ts @@ -0,0 +1,22 @@ +/** + * @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 { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; +import { RootCommands } from '../command-config'; + +export default class ServeCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + multiTarget = false; + command = 'serve [project]'; + aliases = RootCommands['serve'].aliases; + describe = 'Builds and serves your application, rebuilding on file changes.'; + longDescriptionPath?: string | undefined; +} diff --git a/packages/angular/cli/src/commands/test/cli.ts b/packages/angular/cli/src/commands/test/cli.ts new file mode 100644 index 000000000000..600e9f41f517 --- /dev/null +++ b/packages/angular/cli/src/commands/test/cli.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 { join } from 'node:path'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; +import { RootCommands } from '../command-config'; + +export default class TestCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + multiTarget = true; + command = 'test [project]'; + aliases = RootCommands['test'].aliases; + describe = 'Runs unit tests in a project.'; + longDescriptionPath = join(__dirname, 'long-description.md'); +} diff --git a/packages/angular/cli/src/commands/test/long-description.md b/packages/angular/cli/src/commands/test/long-description.md new file mode 100644 index 000000000000..25086c174e15 --- /dev/null +++ b/packages/angular/cli/src/commands/test/long-description.md @@ -0,0 +1,2 @@ +Takes the name of the project, as specified in the `projects` section of the `angular.json` workspace configuration file. +When a project name is not supplied, it will execute for all projects. diff --git a/packages/angular/cli/src/commands/update/cli.ts b/packages/angular/cli/src/commands/update/cli.ts new file mode 100644 index 000000000000..e3b869badff5 --- /dev/null +++ b/packages/angular/cli/src/commands/update/cli.ts @@ -0,0 +1,1230 @@ +/** + * @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 { SchematicDescription, UnsuccessfulWorkflowExecution } from '@angular-devkit/schematics'; +import { + FileSystemCollectionDescription, + FileSystemSchematicDescription, + NodeWorkflow, +} from '@angular-devkit/schematics/tools'; +import { Listr } from 'listr2'; +import { SpawnSyncReturns, execSync, spawnSync } from 'node:child_process'; +import { existsSync, promises as fs } from 'node:fs'; +import { createRequire } from 'node:module'; +import * as path from 'node:path'; +import { join, resolve } from 'node:path'; +import npa from 'npm-package-arg'; +import pickManifest from 'npm-pick-manifest'; +import * as semver from 'semver'; +import { Argv } from 'yargs'; +import { PackageManager } from '../../../lib/config/workspace-schema'; +import { + CommandModule, + CommandModuleError, + CommandScope, + Options, +} from '../../command-builder/command-module'; +import { SchematicEngineHost } from '../../command-builder/utilities/schematic-engine-host'; +import { subscribeToWorkflow } from '../../command-builder/utilities/schematic-workflow'; +import { colors, figures } from '../../utilities/color'; +import { disableVersionCheck } from '../../utilities/environment-options'; +import { assertIsError } from '../../utilities/error'; +import { writeErrorToLogFile } from '../../utilities/log-file'; +import { + PackageIdentifier, + PackageManifest, + fetchPackageManifest, + fetchPackageMetadata, +} from '../../utilities/package-metadata'; +import { + PackageTreeNode, + findPackageJson, + getProjectDependencies, + readPackageJson, +} from '../../utilities/package-tree'; +import { askChoices } from '../../utilities/prompt'; +import { isTTY } from '../../utilities/tty'; +import { VERSION } from '../../utilities/version'; + +interface UpdateCommandArgs { + packages?: string[]; + force: boolean; + next: boolean; + 'migrate-only'?: boolean; + name?: string; + from?: string; + to?: string; + 'allow-dirty': boolean; + verbose: boolean; + 'create-commits': boolean; +} + +interface MigrationSchematicDescription + extends SchematicDescription { + version?: string; + optional?: boolean; + recommended?: boolean; + documentation?: string; +} + +interface MigrationSchematicDescriptionWithVersion extends MigrationSchematicDescription { + version: string; +} + +class CommandError extends Error {} + +const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//; +const UPDATE_SCHEMATIC_COLLECTION = path.join(__dirname, 'schematic/collection.json'); + +export default class UpdateCommandModule extends CommandModule { + override scope = CommandScope.In; + protected override shouldReportAnalytics = false; + private readonly resolvePaths = [__dirname, this.context.root]; + + command = 'update [packages..]'; + describe = 'Updates your workspace and its dependencies. See https://update.angular.dev/.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + + builder(localYargs: Argv): Argv { + return localYargs + .positional('packages', { + description: 'The names of package(s) to update.', + type: 'string', + array: true, + }) + .option('force', { + description: 'Ignore peer dependency version mismatches.', + type: 'boolean', + default: false, + }) + .option('next', { + description: 'Use the prerelease version, including beta and RCs.', + type: 'boolean', + default: false, + }) + .option('migrate-only', { + description: 'Only perform a migration, do not update the installed version.', + type: 'boolean', + }) + .option('name', { + description: + 'The name of the migration to run. Only available when a single package is updated.', + type: 'string', + conflicts: ['to', 'from'], + }) + .option('from', { + description: + 'Version from which to migrate from. ' + + `Only available when a single package is updated, and only with 'migrate-only'.`, + type: 'string', + implies: ['migrate-only'], + conflicts: ['name'], + }) + .option('to', { + describe: + 'Version up to which to apply migrations. Only available when a single package is updated, ' + + `and only with 'migrate-only' option. Requires 'from' to be specified. Default to the installed version detected.`, + type: 'string', + implies: ['from', 'migrate-only'], + conflicts: ['name'], + }) + .option('allow-dirty', { + describe: + 'Whether to allow updating when the repository contains modified or untracked files.', + type: 'boolean', + default: false, + }) + .option('verbose', { + describe: 'Display additional details about internal operations during execution.', + type: 'boolean', + default: false, + }) + .option('create-commits', { + describe: 'Create source control commits for updates and migrations.', + type: 'boolean', + alias: ['C'], + default: false, + }) + .middleware((argv) => { + if (argv.name) { + argv['migrate-only'] = true; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return argv as any; + }) + .check(({ packages, 'allow-dirty': allowDirty, 'migrate-only': migrateOnly }) => { + const { logger } = this.context; + + // This allows the user to easily reset any changes from the update. + if (packages?.length && !this.checkCleanGit()) { + if (allowDirty) { + logger.warn( + 'Repository is not clean. Update changes will be mixed with pre-existing changes.', + ); + } else { + throw new CommandModuleError( + 'Repository is not clean. Please commit or stash any changes before updating.', + ); + } + } + + if (migrateOnly) { + if (packages?.length !== 1) { + throw new CommandModuleError( + `A single package must be specified when using the 'migrate-only' option.`, + ); + } + } + + return true; + }) + .strict(); + } + + async run(options: Options): Promise { + const { logger, packageManager } = this.context; + + // Check if the current installed CLI version is older than the latest compatible version. + // Skip when running `ng update` without a package name as this will not trigger an actual update. + if (!disableVersionCheck && options.packages?.length) { + const cliVersionToInstall = await this.checkCLIVersion( + options.packages, + options.verbose, + options.next, + ); + + if (cliVersionToInstall) { + logger.warn( + 'The installed Angular CLI version is outdated.\n' + + `Installing a temporary Angular CLI versioned ${cliVersionToInstall} to perform the update.`, + ); + + return this.runTempBinary(`@angular/cli@${cliVersionToInstall}`, process.argv.slice(2)); + } + } + + const packages: PackageIdentifier[] = []; + for (const request of options.packages ?? []) { + try { + const packageIdentifier = npa(request); + + // only registry identifiers are supported + if (!packageIdentifier.registry) { + logger.error(`Package '${request}' is not a registry package identifer.`); + + return 1; + } + + if (packages.some((v) => v.name === packageIdentifier.name)) { + logger.error(`Duplicate package '${packageIdentifier.name}' specified.`); + + return 1; + } + + if (options.migrateOnly && packageIdentifier.rawSpec !== '*') { + logger.warn('Package specifier has no effect when using "migrate-only" option.'); + } + + // If next option is used and no specifier supplied, use next tag + if (options.next && packageIdentifier.rawSpec === '*') { + packageIdentifier.fetchSpec = 'next'; + } + + packages.push(packageIdentifier as PackageIdentifier); + } catch (e) { + assertIsError(e); + logger.error(e.message); + + return 1; + } + } + + logger.info(`Using package manager: ${colors.gray(packageManager.name)}`); + logger.info('Collecting installed dependencies...'); + + const rootDependencies = await getProjectDependencies(this.context.root); + logger.info(`Found ${rootDependencies.size} dependencies.`); + + const workflow = new NodeWorkflow(this.context.root, { + packageManager: packageManager.name, + packageManagerForce: this.packageManagerForce(options.verbose), + // __dirname -> favor @schematics/update from this package + // Otherwise, use packages from the active workspace (migrations) + resolvePaths: this.resolvePaths, + schemaValidation: true, + engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), + }); + + if (packages.length === 0) { + // Show status + const { success } = await this.executeSchematic( + workflow, + UPDATE_SCHEMATIC_COLLECTION, + 'update', + { + force: options.force, + next: options.next, + verbose: options.verbose, + packageManager: packageManager.name, + packages: [], + }, + ); + + return success ? 0 : 1; + } + + return options.migrateOnly + ? this.migrateOnly(workflow, (options.packages ?? [])[0], rootDependencies, options) + : this.updatePackagesAndMigrate(workflow, rootDependencies, options, packages); + } + + private async executeSchematic( + workflow: NodeWorkflow, + collection: string, + schematic: string, + options: Record = {}, + ): Promise<{ success: boolean; files: Set }> { + const { logger } = this.context; + const workflowSubscription = subscribeToWorkflow(workflow, logger); + + // TODO: Allow passing a schematic instance directly + try { + await workflow + .execute({ + collection, + schematic, + options, + logger, + }) + .toPromise(); + + return { success: !workflowSubscription.error, files: workflowSubscription.files }; + } catch (e) { + if (e instanceof UnsuccessfulWorkflowExecution) { + logger.error(`${figures.cross} Migration failed. See above for further details.\n`); + } else { + assertIsError(e); + const logPath = writeErrorToLogFile(e); + logger.fatal( + `${figures.cross} Migration failed: ${e.message}\n` + + ` See "${logPath}" for further details.\n`, + ); + } + + return { success: false, files: workflowSubscription.files }; + } finally { + workflowSubscription.unsubscribe(); + } + } + + /** + * @return Whether or not the migration was performed successfully. + */ + private async executeMigration( + workflow: NodeWorkflow, + packageName: string, + collectionPath: string, + migrationName: string, + commit?: boolean, + ): Promise { + const { logger } = this.context; + const collection = workflow.engine.createCollection(collectionPath); + const name = collection.listSchematicNames().find((name) => name === migrationName); + if (!name) { + logger.error(`Cannot find migration '${migrationName}' in '${packageName}'.`); + + return 1; + } + + logger.info(colors.cyan(`** Executing '${migrationName}' of package '${packageName}' **\n`)); + const schematic = workflow.engine.createSchematic(name, collection); + + return this.executePackageMigrations(workflow, [schematic.description], packageName, commit); + } + + /** + * @return Whether or not the migrations were performed successfully. + */ + private async executeMigrations( + workflow: NodeWorkflow, + packageName: string, + collectionPath: string, + from: string, + to: string, + commit?: boolean, + ): Promise { + const collection = workflow.engine.createCollection(collectionPath); + const migrationRange = new semver.Range( + '>' + (semver.prerelease(from) ? from.split('-')[0] + '-0' : from) + ' <=' + to.split('-')[0], + ); + + const requiredMigrations: MigrationSchematicDescriptionWithVersion[] = []; + const optionalMigrations: MigrationSchematicDescriptionWithVersion[] = []; + + for (const name of collection.listSchematicNames()) { + const schematic = workflow.engine.createSchematic(name, collection); + const description = schematic.description as MigrationSchematicDescription; + + description.version = coerceVersionNumber(description.version); + if (!description.version) { + continue; + } + + if (semver.satisfies(description.version, migrationRange, { includePrerelease: true })) { + (description.optional ? optionalMigrations : requiredMigrations).push( + description as MigrationSchematicDescriptionWithVersion, + ); + } + } + + if (requiredMigrations.length === 0 && optionalMigrations.length === 0) { + return 0; + } + + // Required migrations + if (requiredMigrations.length) { + this.context.logger.info( + colors.cyan(`** Executing migrations of package '${packageName}' **\n`), + ); + + requiredMigrations.sort( + (a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name), + ); + + const result = await this.executePackageMigrations( + workflow, + requiredMigrations, + packageName, + commit, + ); + + if (result === 1) { + return 1; + } + } + + // Optional migrations + if (optionalMigrations.length) { + this.context.logger.info( + colors.magenta(`** Optional migrations of package '${packageName}' **\n`), + ); + + optionalMigrations.sort( + (a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name), + ); + + const migrationsToRun = await this.getOptionalMigrationsToRun( + optionalMigrations, + packageName, + ); + + if (migrationsToRun?.length) { + return this.executePackageMigrations(workflow, migrationsToRun, packageName, commit); + } + } + + return 0; + } + + private async executePackageMigrations( + workflow: NodeWorkflow, + migrations: MigrationSchematicDescription[], + packageName: string, + commit = false, + ): Promise<1 | 0> { + const { logger } = this.context; + for (const migration of migrations) { + const { title, description } = getMigrationTitleAndDescription(migration); + + logger.info(colors.cyan(figures.pointer) + ' ' + colors.bold(title)); + + if (description) { + logger.info(' ' + description); + } + + const { success, files } = await this.executeSchematic( + workflow, + migration.collection.name, + migration.name, + ); + if (!success) { + return 1; + } + + let modifiedFilesText: string; + switch (files.size) { + case 0: + modifiedFilesText = 'No changes made'; + break; + case 1: + modifiedFilesText = '1 file modified'; + break; + default: + modifiedFilesText = `${files.size} files modified`; + break; + } + + logger.info(` Migration completed (${modifiedFilesText}).`); + + // Commit migration + if (commit) { + const commitPrefix = `${packageName} migration - ${migration.name}`; + const commitMessage = migration.description + ? `${commitPrefix}\n\n${migration.description}` + : commitPrefix; + const committed = this.commit(commitMessage); + if (!committed) { + // Failed to commit, something went wrong. Abort the update. + return 1; + } + } + + logger.info(''); // Extra trailing newline. + } + + return 0; + } + + private async migrateOnly( + workflow: NodeWorkflow, + packageName: string, + rootDependencies: Map, + options: Options, + ): Promise { + const { logger } = this.context; + const packageDependency = rootDependencies.get(packageName); + let packagePath = packageDependency?.path; + let packageNode = packageDependency?.package; + if (packageDependency && !packageNode) { + logger.error('Package found in package.json but is not installed.'); + + return 1; + } else if (!packageDependency) { + // Allow running migrations on transitively installed dependencies + // There can technically be nested multiple versions + // TODO: If multiple, this should find all versions and ask which one to use + const packageJson = findPackageJson(this.context.root, packageName); + if (packageJson) { + packagePath = path.dirname(packageJson); + packageNode = await readPackageJson(packageJson); + } + } + + if (!packageNode || !packagePath) { + logger.error('Package is not installed.'); + + return 1; + } + + const updateMetadata = packageNode['ng-update']; + let migrations = updateMetadata?.migrations; + if (migrations === undefined) { + logger.error('Package does not provide migrations.'); + + return 1; + } else if (typeof migrations !== 'string') { + logger.error('Package contains a malformed migrations field.'); + + return 1; + } else if (path.posix.isAbsolute(migrations) || path.win32.isAbsolute(migrations)) { + logger.error( + 'Package contains an invalid migrations field. Absolute paths are not permitted.', + ); + + return 1; + } + + // Normalize slashes + migrations = migrations.replace(/\\/g, '/'); + + if (migrations.startsWith('../')) { + logger.error( + 'Package contains an invalid migrations field. Paths outside the package root are not permitted.', + ); + + return 1; + } + + // Check if it is a package-local location + const localMigrations = path.join(packagePath, migrations); + if (existsSync(localMigrations)) { + migrations = localMigrations; + } else { + // Try to resolve from package location. + // This avoids issues with package hoisting. + try { + const packageRequire = createRequire(packagePath + '/'); + migrations = packageRequire.resolve(migrations, { paths: this.resolvePaths }); + } catch (e) { + assertIsError(e); + if (e.code === 'MODULE_NOT_FOUND') { + logger.error('Migrations for package were not found.'); + } else { + logger.error(`Unable to resolve migrations for package. [${e.message}]`); + } + + return 1; + } + } + + if (options.name) { + return this.executeMigration( + workflow, + packageName, + migrations, + options.name, + options.createCommits, + ); + } + + const from = coerceVersionNumber(options.from); + if (!from) { + logger.error(`"from" value [${options.from}] is not a valid version.`); + + return 1; + } + + return this.executeMigrations( + workflow, + packageName, + migrations, + from, + options.to || packageNode.version, + options.createCommits, + ); + } + + // eslint-disable-next-line max-lines-per-function + private async updatePackagesAndMigrate( + workflow: NodeWorkflow, + rootDependencies: Map, + options: Options, + packages: PackageIdentifier[], + ): Promise { + const { logger } = this.context; + + const logVerbose = (message: string) => { + if (options.verbose) { + logger.info(message); + } + }; + + const requests: { + identifier: PackageIdentifier; + node: PackageTreeNode; + }[] = []; + + // Validate packages actually are part of the workspace + for (const pkg of packages) { + const node = rootDependencies.get(pkg.name); + if (!node?.package) { + logger.error(`Package '${pkg.name}' is not a dependency.`); + + return 1; + } + + // If a specific version is requested and matches the installed version, skip. + if (pkg.type === 'version' && node.package.version === pkg.fetchSpec) { + logger.info(`Package '${pkg.name}' is already at '${pkg.fetchSpec}'.`); + continue; + } + + requests.push({ identifier: pkg, node }); + } + + if (requests.length === 0) { + return 0; + } + + logger.info('Fetching dependency metadata from registry...'); + + const packagesToUpdate: string[] = []; + for (const { identifier: requestIdentifier, node } of requests) { + const packageName = requestIdentifier.name; + + let metadata; + try { + // Metadata requests are internally cached; multiple requests for same name + // does not result in additional network traffic + metadata = await fetchPackageMetadata(packageName, logger, { + verbose: options.verbose, + }); + } catch (e) { + assertIsError(e); + logger.error(`Error fetching metadata for '${packageName}': ` + e.message); + + return 1; + } + + // Try to find a package version based on the user requested package specifier + // registry specifier types are either version, range, or tag + let manifest: PackageManifest | undefined; + if ( + requestIdentifier.type === 'version' || + requestIdentifier.type === 'range' || + requestIdentifier.type === 'tag' + ) { + try { + manifest = pickManifest(metadata, requestIdentifier.fetchSpec); + } catch (e) { + assertIsError(e); + if (e.code === 'ETARGET') { + // If not found and next was used and user did not provide a specifier, try latest. + // Package may not have a next tag. + if ( + requestIdentifier.type === 'tag' && + requestIdentifier.fetchSpec === 'next' && + !requestIdentifier.rawSpec + ) { + try { + manifest = pickManifest(metadata, 'latest'); + } catch (e) { + assertIsError(e); + if (e.code !== 'ETARGET' && e.code !== 'ENOVERSIONS') { + throw e; + } + } + } + } else if (e.code !== 'ENOVERSIONS') { + throw e; + } + } + } + + if (!manifest) { + logger.error( + `Package specified by '${requestIdentifier.raw}' does not exist within the registry.`, + ); + + return 1; + } + + if (manifest.version === node.package?.version) { + logger.info(`Package '${packageName}' is already up to date.`); + continue; + } + + if (node.package && ANGULAR_PACKAGES_REGEXP.test(node.package.name)) { + const { name, version } = node.package; + const toBeInstalledMajorVersion = +manifest.version.split('.')[0]; + const currentMajorVersion = +version.split('.')[0]; + + if (toBeInstalledMajorVersion - currentMajorVersion > 1) { + // Only allow updating a single version at a time. + if (currentMajorVersion < 6) { + // Before version 6, the major versions were not always sequential. + // Example @angular/core skipped version 3, @angular/cli skipped versions 2-5. + logger.error( + `Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` + + `For more information about the update process, see https://update.angular.dev/.`, + ); + } else { + const nextMajorVersionFromCurrent = currentMajorVersion + 1; + + logger.error( + `Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` + + `Run 'ng update ${name}@${nextMajorVersionFromCurrent}' in your workspace directory ` + + `to update to latest '${nextMajorVersionFromCurrent}.x' version of '${name}'.\n\n` + + `For more information about the update process, see https://update.angular.dev/?v=${currentMajorVersion}.0-${nextMajorVersionFromCurrent}.0`, + ); + } + + return 1; + } + } + + packagesToUpdate.push(requestIdentifier.toString()); + } + + if (packagesToUpdate.length === 0) { + return 0; + } + + const { success } = await this.executeSchematic( + workflow, + UPDATE_SCHEMATIC_COLLECTION, + 'update', + { + verbose: options.verbose, + force: options.force, + next: options.next, + packageManager: this.context.packageManager.name, + packages: packagesToUpdate, + }, + ); + + if (success) { + const { root: commandRoot, packageManager } = this.context; + const installArgs = this.packageManagerForce(options.verbose) ? ['--force'] : []; + const tasks = new Listr([ + { + title: 'Cleaning node modules directory', + async task(_, task) { + try { + await fs.rm(path.join(commandRoot, 'node_modules'), { + force: true, + recursive: true, + maxRetries: 3, + }); + } catch (e) { + assertIsError(e); + if (e.code === 'ENOENT') { + task.skip('Cleaning not required. Node modules directory not found.'); + } + } + }, + }, + { + title: 'Installing packages', + async task() { + const installationSuccess = await packageManager.installAll(installArgs, commandRoot); + + if (!installationSuccess) { + throw new CommandError('Unable to install packages'); + } + }, + }, + ]); + + try { + await tasks.run(); + } catch (e) { + if (e instanceof CommandError) { + return 1; + } + + throw e; + } + } + + if (success && options.createCommits) { + if (!this.commit(`Angular CLI update for packages - ${packagesToUpdate.join(', ')}`)) { + return 1; + } + } + + // This is a temporary workaround to allow data to be passed back from the update schematic + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const migrations = (global as any).externalMigrations as { + package: string; + collection: string; + from: string; + to: string; + }[]; + + if (success && migrations) { + const rootRequire = createRequire(this.context.root + '/'); + for (const migration of migrations) { + // Resolve the package from the workspace root, as otherwise it will be resolved from the temp + // installed CLI version. + let packagePath; + logVerbose( + `Resolving migration package '${migration.package}' from '${this.context.root}'...`, + ); + try { + try { + packagePath = path.dirname( + // This may fail if the `package.json` is not exported as an entry point + rootRequire.resolve(path.join(migration.package, 'package.json')), + ); + } catch (e) { + assertIsError(e); + if (e.code === 'MODULE_NOT_FOUND') { + // Fallback to trying to resolve the package's main entry point + packagePath = rootRequire.resolve(migration.package); + } else { + throw e; + } + } + } catch (e) { + assertIsError(e); + if (e.code === 'MODULE_NOT_FOUND') { + logVerbose(e.toString()); + logger.error( + `Migrations for package (${migration.package}) were not found.` + + ' The package could not be found in the workspace.', + ); + } else { + logger.error( + `Unable to resolve migrations for package (${migration.package}). [${e.message}]`, + ); + } + + return 1; + } + + let migrations; + + // Check if it is a package-local location + const localMigrations = path.join(packagePath, migration.collection); + if (existsSync(localMigrations)) { + migrations = localMigrations; + } else { + // Try to resolve from package location. + // This avoids issues with package hoisting. + try { + const packageRequire = createRequire(packagePath + '/'); + migrations = packageRequire.resolve(migration.collection); + } catch (e) { + assertIsError(e); + if (e.code === 'MODULE_NOT_FOUND') { + logger.error(`Migrations for package (${migration.package}) were not found.`); + } else { + logger.error( + `Unable to resolve migrations for package (${migration.package}). [${e.message}]`, + ); + } + + return 1; + } + } + const result = await this.executeMigrations( + workflow, + migration.package, + migrations, + migration.from, + migration.to, + options.createCommits, + ); + + // A non-zero value is a failure for the package's migrations + if (result !== 0) { + return result; + } + } + } + + return success ? 0 : 1; + } + + /** + * @return Whether or not the commit was successful. + */ + private commit(message: string): boolean { + const { logger } = this.context; + + // Check if a commit is needed. + let commitNeeded: boolean; + try { + commitNeeded = hasChangesToCommit(); + } catch (err) { + logger.error(` Failed to read Git tree:\n${(err as SpawnSyncReturns).stderr}`); + + return false; + } + + if (!commitNeeded) { + logger.info(' No changes to commit after migration.'); + + return true; + } + + // Commit changes and abort on error. + try { + createCommit(message); + } catch (err) { + logger.error( + `Failed to commit update (${message}):\n${(err as SpawnSyncReturns).stderr}`, + ); + + return false; + } + + // Notify user of the commit. + const hash = findCurrentGitSha(); + const shortMessage = message.split('\n')[0]; + if (hash) { + logger.info(` Committed migration step (${getShortHash(hash)}): ${shortMessage}.`); + } else { + // Commit was successful, but reading the hash was not. Something weird happened, + // but nothing that would stop the update. Just log the weirdness and continue. + logger.info(` Committed migration step: ${shortMessage}.`); + logger.warn(' Failed to look up hash of most recent commit, continuing anyways.'); + } + + return true; + } + + private checkCleanGit(): boolean { + try { + const topLevel = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + stdio: 'pipe', + }); + const result = execSync('git status --porcelain', { encoding: 'utf8', stdio: 'pipe' }); + if (result.trim().length === 0) { + return true; + } + + // Only files inside the workspace root are relevant + for (const entry of result.split('\n')) { + const relativeEntry = path.relative( + path.resolve(this.context.root), + path.resolve(topLevel.trim(), entry.slice(3).trim()), + ); + + if (!relativeEntry.startsWith('..') && !path.isAbsolute(relativeEntry)) { + return false; + } + } + } catch {} + + return true; + } + + /** + * Checks if the current installed CLI version is older or newer than a compatible version. + * @returns the version to install or null when there is no update to install. + */ + private async checkCLIVersion( + packagesToUpdate: string[], + verbose = false, + next = false, + ): Promise { + const { version } = await fetchPackageManifest( + `@angular/cli@${this.getCLIUpdateRunnerVersion(packagesToUpdate, next)}`, + this.context.logger, + { + verbose, + usingYarn: this.context.packageManager.name === PackageManager.Yarn, + }, + ); + + return VERSION.full === version ? null : version; + } + + private getCLIUpdateRunnerVersion( + packagesToUpdate: string[] | undefined, + next: boolean, + ): string | number { + if (next) { + return 'next'; + } + + const updatingAngularPackage = packagesToUpdate?.find((r) => ANGULAR_PACKAGES_REGEXP.test(r)); + if (updatingAngularPackage) { + // If we are updating any Angular package we can update the CLI to the target version because + // migrations for @angular/core@13 can be executed using Angular/cli@13. + // This is same behaviour as `npx @angular/cli@13 update @angular/core@13`. + + // `@angular/cli@13` -> ['', 'angular/cli', '13'] + // `@angular/cli` -> ['', 'angular/cli'] + const tempVersion = coerceVersionNumber(updatingAngularPackage.split('@')[2]); + + return semver.parse(tempVersion)?.major ?? 'latest'; + } + + // When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in. + // Typically, we can assume that the `@angular/cli` was updated previously. + // Example: Angular official packages are typically updated prior to NGRX etc... + // Therefore, we only update to the latest patch version of the installed major version of the Angular CLI. + + // This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12. + // We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic. + return VERSION.major; + } + + private async runTempBinary(packageName: string, args: string[] = []): Promise { + const { success, tempNodeModules } = await this.context.packageManager.installTemp(packageName); + if (!success) { + return 1; + } + + // Remove version/tag etc... from package name + // Ex: @angular/cli@latest -> @angular/cli + const packageNameNoVersion = packageName.substring(0, packageName.lastIndexOf('@')); + const pkgLocation = join(tempNodeModules, packageNameNoVersion); + const packageJsonPath = join(pkgLocation, 'package.json'); + + // Get a binary location for this package + let binPath: string | undefined; + if (existsSync(packageJsonPath)) { + const content = await fs.readFile(packageJsonPath, 'utf-8'); + if (content) { + const { bin = {} } = JSON.parse(content) as { bin: Record }; + const binKeys = Object.keys(bin); + + if (binKeys.length) { + binPath = resolve(pkgLocation, bin[binKeys[0]]); + } + } + } + + if (!binPath) { + throw new Error(`Cannot locate bin for temporary package: ${packageNameNoVersion}.`); + } + + const { status, error } = spawnSync(process.execPath, [binPath, ...args], { + stdio: 'inherit', + env: { + ...process.env, + NG_DISABLE_VERSION_CHECK: 'true', + NG_CLI_ANALYTICS: 'false', + }, + }); + + if (status === null && error) { + throw error; + } + + return status ?? 0; + } + + private packageManagerForce(verbose: boolean): boolean { + // npm 7+ can fail due to it incorrectly resolving peer dependencies that have valid SemVer + // ranges during an update. Update will set correct versions of dependencies within the + // package.json file. The force option is set to workaround these errors. + // Example error: + // npm ERR! Conflicting peer dependency: @angular/compiler-cli@14.0.0-rc.0 + // npm ERR! node_modules/@angular/compiler-cli + // npm ERR! peer @angular/compiler-cli@"^14.0.0 || ^14.0.0-rc" from @angular-devkit/build-angular@14.0.0-rc.0 + // npm ERR! node_modules/@angular-devkit/build-angular + // npm ERR! dev @angular-devkit/build-angular@"~14.0.0-rc.0" from the root project + if ( + this.context.packageManager.name === PackageManager.Npm && + this.context.packageManager.version && + semver.gte(this.context.packageManager.version, '7.0.0') + ) { + if (verbose) { + this.context.logger.info( + 'NPM 7+ detected -- enabling force option for package installation', + ); + } + + return true; + } + + return false; + } + + private async getOptionalMigrationsToRun( + optionalMigrations: MigrationSchematicDescription[], + packageName: string, + ): Promise { + const { logger } = this.context; + const numberOfMigrations = optionalMigrations.length; + logger.info( + `This package has ${numberOfMigrations} optional migration${ + numberOfMigrations > 1 ? 's' : '' + } that can be executed.`, + ); + + if (!isTTY()) { + for (const migration of optionalMigrations) { + const { title } = getMigrationTitleAndDescription(migration); + logger.info(colors.cyan(figures.pointer) + ' ' + colors.bold(title)); + logger.info(colors.gray(` ng update ${packageName} --name ${migration.name}`)); + logger.info(''); // Extra trailing newline. + } + + return undefined; + } + + logger.info( + 'Optional migrations may be skipped and executed after the update process, if preferred.', + ); + logger.info(''); // Extra trailing newline. + + const answer = await askChoices( + `Select the migrations that you'd like to run`, + optionalMigrations.map((migration) => { + const { title, documentation } = getMigrationTitleAndDescription(migration); + + return { + name: `[${colors.white(migration.name)}] ${title}${documentation ? ` (${documentation})` : ''}`, + value: migration.name, + checked: migration.recommended, + }; + }), + null, + ); + + logger.info(''); // Extra trailing newline. + + return optionalMigrations.filter(({ name }) => answer?.includes(name)); + } +} + +/** + * @return Whether or not the working directory has Git changes to commit. + */ +function hasChangesToCommit(): boolean { + // List all modified files not covered by .gitignore. + // If any files are returned, then there must be something to commit. + + return execSync('git ls-files -m -d -o --exclude-standard').toString() !== ''; +} + +/** + * Precondition: Must have pending changes to commit, they do not need to be staged. + * Postcondition: The Git working tree is committed and the repo is clean. + * @param message The commit message to use. + */ +function createCommit(message: string) { + // Stage entire working tree for commit. + execSync('git add -A', { encoding: 'utf8', stdio: 'pipe' }); + + // Commit with the message passed via stdin to avoid bash escaping issues. + execSync('git commit --no-verify -F -', { encoding: 'utf8', stdio: 'pipe', input: message }); +} + +/** + * @return The Git SHA hash of the HEAD commit. Returns null if unable to retrieve the hash. + */ +function findCurrentGitSha(): string | null { + try { + return execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: 'pipe' }).trim(); + } catch { + return null; + } +} + +function getShortHash(commitHash: string): string { + return commitHash.slice(0, 9); +} + +function coerceVersionNumber(version: string | undefined): string | undefined { + if (!version) { + return undefined; + } + + if (!/^\d{1,30}\.\d{1,30}\.\d{1,30}/.test(version)) { + const match = version.match(/^\d{1,30}(\.\d{1,30})*/); + + if (!match) { + return undefined; + } + + if (!match[1]) { + version = version.substring(0, match[0].length) + '.0.0' + version.substring(match[0].length); + } else if (!match[2]) { + version = version.substring(0, match[0].length) + '.0' + version.substring(match[0].length); + } else { + return undefined; + } + } + + return semver.valid(version) ?? undefined; +} + +function getMigrationTitleAndDescription(migration: MigrationSchematicDescription): { + title: string; + description: string; + documentation?: string; +} { + const [title, ...description] = migration.description.split('. '); + + return { + title: title.endsWith('.') ? title : title + '.', + description: description.join('.\n '), + documentation: migration.documentation + ? new URL(migration.documentation, 'https://angular.dev/').href + : undefined, + }; +} diff --git a/packages/angular/cli/src/commands/update/long-description.md b/packages/angular/cli/src/commands/update/long-description.md new file mode 100644 index 000000000000..612971de0c4d --- /dev/null +++ b/packages/angular/cli/src/commands/update/long-description.md @@ -0,0 +1,22 @@ +Perform a basic update to the current stable release of the core framework and CLI by running the following command. + +``` +ng update @angular/cli @angular/core +``` + +To update to the next beta or pre-release version, use the `--next` option. + +To update from one major version to another, use the format + +``` +ng update @angular/cli@^ @angular/core@^ +``` + +We recommend that you always update to the latest patch version, as it contains fixes we released since the initial major release. +For example, use the following command to take the latest 10.x.x version and use that to update. + +``` +ng update @angular/cli@^10 @angular/core@^10 +``` + +For detailed information and guidance on updating your application, see the interactive [Angular Update Guide](https://update.angular.dev/). diff --git a/packages/angular/cli/src/commands/update/schematic/collection.json b/packages/angular/cli/src/commands/update/schematic/collection.json new file mode 100644 index 000000000000..ee7197918bd6 --- /dev/null +++ b/packages/angular/cli/src/commands/update/schematic/collection.json @@ -0,0 +1,9 @@ +{ + "schematics": { + "update": { + "factory": "./index", + "schema": "./schema.json", + "description": "Update one or multiple packages to versions, updating peer dependencies along the way." + } + } +} diff --git a/packages/angular/cli/src/commands/update/schematic/index.ts b/packages/angular/cli/src/commands/update/schematic/index.ts new file mode 100644 index 000000000000..26d2d06836b4 --- /dev/null +++ b/packages/angular/cli/src/commands/update/schematic/index.ts @@ -0,0 +1,933 @@ +/** + * @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 { Rule, SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; +import * as npa from 'npm-package-arg'; +import type { Manifest } from 'pacote'; +import * as semver from 'semver'; +import { + NgPackageManifestProperties, + NpmRepositoryPackageJson, + getNpmPackageJson, +} from '../../../utilities/package-metadata'; +import { Schema as UpdateSchema } from './schema'; + +interface JsonSchemaForNpmPackageJsonFiles extends Manifest, NgPackageManifestProperties { + peerDependenciesMeta?: Record; +} + +type VersionRange = string & { __VERSION_RANGE: void }; +type PeerVersionTransform = string | ((range: string) => string); + +// Angular guarantees that a major is compatible with its following major (so packages that depend +// on Angular 5 are also compatible with Angular 6). This is, in code, represented by verifying +// that all other packages that have a peer dependency of `"@angular/core": "^5.0.0"` actually +// supports 6.0, by adding that compatibility to the range, so it is `^5.0.0 || ^6.0.0`. +// We export it to allow for testing. +export function angularMajorCompatGuarantee(range: string) { + let newRange = semver.validRange(range); + if (!newRange) { + return range; + } + let major = 1; + while (!semver.gtr(major + '.0.0', newRange)) { + major++; + if (major >= 99) { + // Use original range if it supports a major this high + // Range is most likely unbounded (e.g., >=5.0.0) + return newRange; + } + } + + // Add the major version as compatible with the angular compatible, with all minors. This is + // already one major above the greatest supported, because we increment `major` before checking. + // We add minors like this because a minor beta is still compatible with a minor non-beta. + newRange = range; + for (let minor = 0; minor < 20; minor++) { + newRange += ` || ^${major}.${minor}.0-alpha.0 `; + } + + return semver.validRange(newRange) || range; +} + +// This is a map of packageGroupName to range extending function. If it isn't found, the range is +// kept the same. +const knownPeerCompatibleList: { [name: string]: PeerVersionTransform } = { + '@angular/core': angularMajorCompatGuarantee, +}; + +interface PackageVersionInfo { + version: VersionRange; + packageJson: JsonSchemaForNpmPackageJsonFiles; + updateMetadata: UpdateMetadata; +} + +interface PackageInfo { + name: string; + npmPackageJson: NpmRepositoryPackageJson; + installed: PackageVersionInfo; + target?: PackageVersionInfo; + packageJsonRange: string; +} + +interface UpdateMetadata { + packageGroupName?: string; + packageGroup: { [packageName: string]: string }; + requirements: { [packageName: string]: string }; + migrations?: string; +} + +function _updatePeerVersion(infoMap: Map, name: string, range: string) { + // Resolve packageGroupName. + const maybePackageInfo = infoMap.get(name); + if (!maybePackageInfo) { + return range; + } + if (maybePackageInfo.target) { + name = maybePackageInfo.target.updateMetadata.packageGroupName || name; + } else { + name = maybePackageInfo.installed.updateMetadata.packageGroupName || name; + } + + const maybeTransform = knownPeerCompatibleList[name]; + if (maybeTransform) { + if (typeof maybeTransform == 'function') { + return maybeTransform(range); + } else { + return maybeTransform; + } + } + + return range; +} + +function _validateForwardPeerDependencies( + name: string, + infoMap: Map, + peers: { [name: string]: string }, + peersMeta: { [name: string]: { optional?: boolean } }, + logger: logging.LoggerApi, + next: boolean, +): boolean { + let validationFailed = false; + for (const [peer, range] of Object.entries(peers)) { + logger.debug(`Checking forward peer ${peer}...`); + const maybePeerInfo = infoMap.get(peer); + const isOptional = peersMeta[peer] && !!peersMeta[peer].optional; + if (!maybePeerInfo) { + if (!isOptional) { + logger.warn( + [ + `Package ${JSON.stringify(name)} has a missing peer dependency of`, + `${JSON.stringify(peer)} @ ${JSON.stringify(range)}.`, + ].join(' '), + ); + } + + continue; + } + + const peerVersion = + maybePeerInfo.target && maybePeerInfo.target.packageJson.version + ? maybePeerInfo.target.packageJson.version + : maybePeerInfo.installed.version; + + logger.debug(` Range intersects(${range}, ${peerVersion})...`); + if (!semver.satisfies(peerVersion, range, { includePrerelease: next || undefined })) { + logger.error( + [ + `Package ${JSON.stringify(name)} has an incompatible peer dependency to`, + `${JSON.stringify(peer)} (requires ${JSON.stringify(range)},`, + `would install ${JSON.stringify(peerVersion)})`, + ].join(' '), + ); + + validationFailed = true; + continue; + } + } + + return validationFailed; +} + +function _validateReversePeerDependencies( + name: string, + version: string, + infoMap: Map, + logger: logging.LoggerApi, + next: boolean, +) { + for (const [installed, installedInfo] of infoMap.entries()) { + const installedLogger = logger.createChild(installed); + installedLogger.debug(`${installed}...`); + const peers = (installedInfo.target || installedInfo.installed).packageJson.peerDependencies; + + for (const [peer, range] of Object.entries(peers || {})) { + if (peer != name) { + // Only check peers to the packages we're updating. We don't care about peers + // that are unmet but we have no effect on. + continue; + } + + // Ignore peerDependency mismatches for these packages. + // They are deprecated and removed via a migration. + const ignoredPackages = [ + 'codelyzer', + '@schematics/update', + '@angular-devkit/build-ng-packagr', + 'tsickle', + '@nguniversal/builders', + ]; + if (ignoredPackages.includes(installed)) { + continue; + } + + // Override the peer version range if it's known as a compatible. + const extendedRange = _updatePeerVersion(infoMap, peer, range); + + if (!semver.satisfies(version, extendedRange, { includePrerelease: next || undefined })) { + logger.error( + [ + `Package ${JSON.stringify(installed)} has an incompatible peer dependency to`, + `${JSON.stringify(name)} (requires`, + `${JSON.stringify(range)}${extendedRange == range ? '' : ' (extended)'},`, + `would install ${JSON.stringify(version)}).`, + ].join(' '), + ); + + return true; + } + } + } + + return false; +} + +function _validateUpdatePackages( + infoMap: Map, + force: boolean, + next: boolean, + logger: logging.LoggerApi, +): void { + logger.debug('Updating the following packages:'); + infoMap.forEach((info) => { + if (info.target) { + logger.debug(` ${info.name} => ${info.target.version}`); + } + }); + + let peerErrors = false; + infoMap.forEach((info) => { + const { name, target } = info; + if (!target) { + return; + } + + const pkgLogger = logger.createChild(name); + logger.debug(`${name}...`); + + const { peerDependencies = {}, peerDependenciesMeta = {} } = target.packageJson; + peerErrors = + _validateForwardPeerDependencies( + name, + infoMap, + peerDependencies, + peerDependenciesMeta, + pkgLogger, + next, + ) || peerErrors; + peerErrors = + _validateReversePeerDependencies(name, target.version, infoMap, pkgLogger, next) || + peerErrors; + }); + + if (!force && peerErrors) { + throw new SchematicsException( + 'Incompatible peer dependencies found.\n' + + 'Peer dependency warnings when installing dependencies means that those dependencies might not work correctly together.\n' + + `You can use the '--force' option to ignore incompatible peer dependencies and instead address these warnings later.`, + ); + } +} + +function _performUpdate( + tree: Tree, + context: SchematicContext, + infoMap: Map, + logger: logging.LoggerApi, + migrateOnly: boolean, +): void { + const packageJsonContent = tree.read('/package.json')?.toString(); + if (!packageJsonContent) { + throw new SchematicsException('Could not find a package.json. Are you in a Node project?'); + } + + const packageJson = tree.readJson('/package.json') as JsonSchemaForNpmPackageJsonFiles; + + const updateDependency = (deps: Record, name: string, newVersion: string) => { + const oldVersion = deps[name]; + // We only respect caret and tilde ranges on update. + const execResult = /^[\^~]/.exec(oldVersion); + deps[name] = `${execResult ? execResult[0] : ''}${newVersion}`; + }; + + const toInstall = [...infoMap.values()] + .map((x) => [x.name, x.target, x.installed]) + .filter(([name, target, installed]) => { + return !!name && !!target && !!installed; + }) as [string, PackageVersionInfo, PackageVersionInfo][]; + + toInstall.forEach(([name, target, installed]) => { + logger.info( + `Updating package.json with dependency ${name} ` + + `@ ${JSON.stringify(target.version)} (was ${JSON.stringify(installed.version)})...`, + ); + + if (packageJson.dependencies && packageJson.dependencies[name]) { + updateDependency(packageJson.dependencies, name, target.version); + + if (packageJson.devDependencies && packageJson.devDependencies[name]) { + delete packageJson.devDependencies[name]; + } + if (packageJson.peerDependencies && packageJson.peerDependencies[name]) { + delete packageJson.peerDependencies[name]; + } + } else if (packageJson.devDependencies && packageJson.devDependencies[name]) { + updateDependency(packageJson.devDependencies, name, target.version); + + if (packageJson.peerDependencies && packageJson.peerDependencies[name]) { + delete packageJson.peerDependencies[name]; + } + } else if (packageJson.peerDependencies && packageJson.peerDependencies[name]) { + updateDependency(packageJson.peerDependencies, name, target.version); + } else { + logger.warn(`Package ${name} was not found in dependencies.`); + } + }); + const eofMatches = packageJsonContent.match(/\r?\n$/); + const eof = eofMatches?.[0] ?? ''; + const newContent = JSON.stringify(packageJson, null, 2) + eof; + if (packageJsonContent != newContent || migrateOnly) { + if (!migrateOnly) { + tree.overwrite('/package.json', newContent); + } + + const externalMigrations: {}[] = []; + + // Run the migrate schematics with the list of packages to use. The collection contains + // version information and we need to do this post installation. Please note that the + // migration COULD fail and leave side effects on disk. + // Run the schematics task of those packages. + toInstall.forEach(([name, target, installed]) => { + if (!target.updateMetadata.migrations) { + return; + } + + externalMigrations.push({ + package: name, + collection: target.updateMetadata.migrations, + from: installed.version, + to: target.version, + }); + + return; + }); + + if (externalMigrations.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global as any).externalMigrations = externalMigrations; + } + } +} + +function _getUpdateMetadata( + packageJson: JsonSchemaForNpmPackageJsonFiles, + logger: logging.LoggerApi, +): UpdateMetadata { + const metadata = packageJson['ng-update']; + + const result: UpdateMetadata = { + packageGroup: {}, + requirements: {}, + }; + + if (!metadata || typeof metadata != 'object' || Array.isArray(metadata)) { + return result; + } + + if (metadata['packageGroup']) { + const packageGroup = metadata['packageGroup']; + // Verify that packageGroup is an array of strings or an map of versions. This is not an error + // but we still warn the user and ignore the packageGroup keys. + if (Array.isArray(packageGroup) && packageGroup.every((x) => typeof x == 'string')) { + result.packageGroup = packageGroup.reduce((group, name) => { + group[name] = packageJson.version; + + return group; + }, result.packageGroup); + } else if ( + typeof packageGroup == 'object' && + packageGroup && + !Array.isArray(packageGroup) && + Object.values(packageGroup).every((x) => typeof x == 'string') + ) { + result.packageGroup = packageGroup; + } else { + logger.warn(`packageGroup metadata of package ${packageJson.name} is malformed. Ignoring.`); + } + + result.packageGroupName = Object.keys(result.packageGroup)[0]; + } + + if (typeof metadata['packageGroupName'] == 'string') { + result.packageGroupName = metadata['packageGroupName']; + } + + if (metadata['requirements']) { + const requirements = metadata['requirements']; + // Verify that requirements are + if ( + typeof requirements != 'object' || + Array.isArray(requirements) || + Object.keys(requirements).some((name) => typeof requirements[name] != 'string') + ) { + logger.warn(`requirements metadata of package ${packageJson.name} is malformed. Ignoring.`); + } else { + result.requirements = requirements; + } + } + + if (metadata['migrations']) { + const migrations = metadata['migrations']; + if (typeof migrations != 'string') { + logger.warn(`migrations metadata of package ${packageJson.name} is malformed. Ignoring.`); + } else { + result.migrations = migrations; + } + } + + return result; +} + +function _usageMessage( + options: UpdateSchema, + infoMap: Map, + logger: logging.LoggerApi, +) { + const packageGroups = new Map(); + const packagesToUpdate = [...infoMap.entries()] + .map(([name, info]) => { + let tag = options.next + ? info.npmPackageJson['dist-tags']['next'] + ? 'next' + : 'latest' + : 'latest'; + let version = info.npmPackageJson['dist-tags'][tag]; + let target = info.npmPackageJson.versions[version]; + + const versionDiff = semver.diff(info.installed.version, version); + if ( + versionDiff !== 'patch' && + versionDiff !== 'minor' && + /^@(?:angular|nguniversal)\//.test(name) + ) { + const installedMajorVersion = semver.parse(info.installed.version)?.major; + const toInstallMajorVersion = semver.parse(version)?.major; + if ( + installedMajorVersion !== undefined && + toInstallMajorVersion !== undefined && + installedMajorVersion < toInstallMajorVersion - 1 + ) { + const nextMajorVersion = `${installedMajorVersion + 1}.`; + const nextMajorVersions = Object.keys(info.npmPackageJson.versions) + .filter((v) => v.startsWith(nextMajorVersion)) + .sort((a, b) => (a > b ? -1 : 1)); + + if (nextMajorVersions.length) { + version = nextMajorVersions[0]; + target = info.npmPackageJson.versions[version]; + tag = ''; + } + } + } + + return { + name, + info, + version, + tag, + target, + }; + }) + .filter( + ({ info, version, target }) => + target?.['ng-update'] && semver.compare(info.installed.version, version) < 0, + ) + .map(({ name, info, version, tag, target }) => { + // Look for packageGroup. + const ngUpdate = target['ng-update']; + const packageGroup = ngUpdate?.['packageGroup']; + if (packageGroup) { + const packageGroupNames = Array.isArray(packageGroup) + ? packageGroup + : Object.keys(packageGroup); + const packageGroupName = + ngUpdate?.['packageGroupName'] || packageGroupNames.find((n) => infoMap.has(n)); + + if (packageGroupName) { + if (packageGroups.has(name)) { + return null; + } + + for (const groupName of packageGroupNames) { + packageGroups.set(groupName, packageGroupName); + } + + packageGroups.set(packageGroupName, packageGroupName); + name = packageGroupName; + } + } + + let command = `ng update ${name}`; + if (!tag) { + command += `@${semver.parse(version)?.major || version}`; + } else if (tag == 'next') { + command += ' --next'; + } + + return [name, `${info.installed.version} -> ${version} `, command]; + }) + .filter((x) => x !== null) + .sort((a, b) => (a && b ? a[0].localeCompare(b[0]) : 0)); + + if (packagesToUpdate.length == 0) { + logger.info('We analyzed your package.json and everything seems to be in order. Good work!'); + + return; + } + + logger.info('We analyzed your package.json, there are some packages to update:\n'); + + // Find the largest name to know the padding needed. + let namePad = Math.max(...[...infoMap.keys()].map((x) => x.length)) + 2; + if (!Number.isFinite(namePad)) { + namePad = 30; + } + const pads = [namePad, 25, 0]; + + logger.info( + ' ' + ['Name', 'Version', 'Command to update'].map((x, i) => x.padEnd(pads[i])).join(''), + ); + logger.info(' ' + '-'.repeat(pads.reduce((s, x) => (s += x), 0) + 20)); + + packagesToUpdate.forEach((fields) => { + if (!fields) { + return; + } + + logger.info(' ' + fields.map((x, i) => x.padEnd(pads[i])).join('')); + }); + + logger.info( + `\nThere might be additional packages which don't provide 'ng update' capabilities that are outdated.\n` + + `You can update the additional packages by running the update command of your package manager.`, + ); + + return; +} + +function _buildPackageInfo( + tree: Tree, + packages: Map, + allDependencies: ReadonlyMap, + npmPackageJson: NpmRepositoryPackageJson, + logger: logging.LoggerApi, +): PackageInfo { + const name = npmPackageJson.name; + const packageJsonRange = allDependencies.get(name); + if (!packageJsonRange) { + throw new SchematicsException(`Package ${JSON.stringify(name)} was not found in package.json.`); + } + + // Find out the currently installed version. Either from the package.json or the node_modules/ + // TODO: figure out a way to read package-lock.json and/or yarn.lock. + const pkgJsonPath = `/node_modules/${name}/package.json`; + const pkgJsonExists = tree.exists(pkgJsonPath); + + let installedVersion: string | undefined | null; + if (pkgJsonExists) { + const { version } = tree.readJson(pkgJsonPath) as JsonSchemaForNpmPackageJsonFiles; + installedVersion = version; + } + + const packageVersionsNonDeprecated: string[] = []; + const packageVersionsDeprecated: string[] = []; + + for (const [version, { deprecated }] of Object.entries(npmPackageJson.versions)) { + if (deprecated) { + packageVersionsDeprecated.push(version); + } else { + packageVersionsNonDeprecated.push(version); + } + } + + const findSatisfyingVersion = (targetVersion: VersionRange): VersionRange | undefined => + ((semver.maxSatisfying(packageVersionsNonDeprecated, targetVersion) ?? + semver.maxSatisfying(packageVersionsDeprecated, targetVersion)) as VersionRange | null) ?? + undefined; + + if (!installedVersion) { + // Find the version from NPM that fits the range to max. + installedVersion = findSatisfyingVersion(packageJsonRange); + } + + if (!installedVersion) { + throw new SchematicsException( + `An unexpected error happened; could not determine version for package ${name}.`, + ); + } + + const installedPackageJson = npmPackageJson.versions[installedVersion] || pkgJsonExists; + if (!installedPackageJson) { + throw new SchematicsException( + `An unexpected error happened; package ${name} has no version ${installedVersion}.`, + ); + } + + let targetVersion: VersionRange | undefined = packages.get(name); + if (targetVersion) { + if (npmPackageJson['dist-tags'][targetVersion]) { + targetVersion = npmPackageJson['dist-tags'][targetVersion] as VersionRange; + } else if (targetVersion == 'next') { + targetVersion = npmPackageJson['dist-tags']['latest'] as VersionRange; + } else { + targetVersion = findSatisfyingVersion(targetVersion); + } + } + + if (targetVersion && semver.lte(targetVersion, installedVersion)) { + logger.debug(`Package ${name} already satisfied by package.json (${packageJsonRange}).`); + targetVersion = undefined; + } + + const target: PackageVersionInfo | undefined = targetVersion + ? { + version: targetVersion, + packageJson: npmPackageJson.versions[targetVersion], + updateMetadata: _getUpdateMetadata(npmPackageJson.versions[targetVersion], logger), + } + : undefined; + + // Check if there's an installed version. + return { + name, + npmPackageJson, + installed: { + version: installedVersion as VersionRange, + packageJson: installedPackageJson, + updateMetadata: _getUpdateMetadata(installedPackageJson, logger), + }, + target, + packageJsonRange, + }; +} + +function _buildPackageList( + options: UpdateSchema, + projectDeps: Map, + logger: logging.LoggerApi, +): Map { + // Parse the packages options to set the targeted version. + const packages = new Map(); + const commandLinePackages = + options.packages && options.packages.length > 0 ? options.packages : []; + + for (const pkg of commandLinePackages) { + // Split the version asked on command line. + const m = pkg.match(/^((?:@[^/]{1,100}\/)?[^@]{1,100})(?:@(.{1,100}))?$/); + if (!m) { + logger.warn(`Invalid package argument: ${JSON.stringify(pkg)}. Skipping.`); + continue; + } + + const [, npmName, maybeVersion] = m; + + const version = projectDeps.get(npmName); + if (!version) { + logger.warn(`Package not installed: ${JSON.stringify(npmName)}. Skipping.`); + continue; + } + + packages.set(npmName, (maybeVersion || (options.next ? 'next' : 'latest')) as VersionRange); + } + + return packages; +} + +function _addPackageGroup( + tree: Tree, + packages: Map, + allDependencies: ReadonlyMap, + npmPackageJson: NpmRepositoryPackageJson, + logger: logging.LoggerApi, +): void { + const maybePackage = packages.get(npmPackageJson.name); + if (!maybePackage) { + return; + } + + const info = _buildPackageInfo(tree, packages, allDependencies, npmPackageJson, logger); + + const version = + (info.target && info.target.version) || + npmPackageJson['dist-tags'][maybePackage] || + maybePackage; + if (!npmPackageJson.versions[version]) { + return; + } + const ngUpdateMetadata = npmPackageJson.versions[version]['ng-update']; + if (!ngUpdateMetadata) { + return; + } + + const packageGroup = ngUpdateMetadata['packageGroup']; + if (!packageGroup) { + return; + } + let packageGroupNormalized: Record = {}; + if (Array.isArray(packageGroup) && !packageGroup.some((x) => typeof x != 'string')) { + packageGroupNormalized = packageGroup.reduce( + (acc, curr) => { + acc[curr] = maybePackage; + + return acc; + }, + {} as { [name: string]: string }, + ); + } else if ( + typeof packageGroup == 'object' && + packageGroup && + !Array.isArray(packageGroup) && + Object.values(packageGroup).every((x) => typeof x == 'string') + ) { + packageGroupNormalized = packageGroup; + } else { + logger.warn(`packageGroup metadata of package ${npmPackageJson.name} is malformed. Ignoring.`); + + return; + } + + for (const [name, value] of Object.entries(packageGroupNormalized)) { + // Don't override names from the command line. + // Remove packages that aren't installed. + if (!packages.has(name) && allDependencies.has(name)) { + packages.set(name, value as VersionRange); + } + } +} + +/** + * Add peer dependencies of packages on the command line to the list of packages to update. + * We don't do verification of the versions here as this will be done by a later step (and can + * be ignored by the --force flag). + * @private + */ +function _addPeerDependencies( + tree: Tree, + packages: Map, + allDependencies: ReadonlyMap, + npmPackageJson: NpmRepositoryPackageJson, + npmPackageJsonMap: Map, + logger: logging.LoggerApi, +): void { + const maybePackage = packages.get(npmPackageJson.name); + if (!maybePackage) { + return; + } + + const info = _buildPackageInfo(tree, packages, allDependencies, npmPackageJson, logger); + + const version = + (info.target && info.target.version) || + npmPackageJson['dist-tags'][maybePackage] || + maybePackage; + if (!npmPackageJson.versions[version]) { + return; + } + + const packageJson = npmPackageJson.versions[version]; + const error = false; + + for (const [peer, range] of Object.entries(packageJson.peerDependencies || {})) { + if (packages.has(peer)) { + continue; + } + + const peerPackageJson = npmPackageJsonMap.get(peer); + if (peerPackageJson) { + const peerInfo = _buildPackageInfo(tree, packages, allDependencies, peerPackageJson, logger); + if (semver.satisfies(peerInfo.installed.version, range)) { + continue; + } + } + + packages.set(peer, range as VersionRange); + } + + if (error) { + throw new SchematicsException('An error occured, see above.'); + } +} + +function _getAllDependencies(tree: Tree): Array { + const { dependencies, devDependencies, peerDependencies } = tree.readJson( + '/package.json', + ) as JsonSchemaForNpmPackageJsonFiles; + + return [ + ...(Object.entries(peerDependencies || {}) as Array<[string, VersionRange]>), + ...(Object.entries(devDependencies || {}) as Array<[string, VersionRange]>), + ...(Object.entries(dependencies || {}) as Array<[string, VersionRange]>), + ]; +} + +function _formatVersion(version: string | undefined) { + if (version === undefined) { + return undefined; + } + + if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) { + version += '.0'; + } + if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) { + version += '.0'; + } + if (!semver.valid(version)) { + throw new SchematicsException(`Invalid migration version: ${JSON.stringify(version)}`); + } + + return version; +} + +/** + * Returns whether or not the given package specifier (the value string in a + * `package.json` dependency) is hosted in the NPM registry. + * @throws When the specifier cannot be parsed. + */ +function isPkgFromRegistry(name: string, specifier: string): boolean { + const result = npa.resolve(name, specifier); + + return !!result.registry; +} + +export default function (options: UpdateSchema): Rule { + if (!options.packages) { + // We cannot just return this because we need to fetch the packages from NPM still for the + // help/guide to show. + options.packages = []; + } else { + // We split every packages by commas to allow people to pass in multiple and make it an array. + options.packages = options.packages.reduce((acc, curr) => { + return acc.concat(curr.split(',')); + }, [] as string[]); + } + + if (options.migrateOnly && options.from) { + if (options.packages.length !== 1) { + throw new SchematicsException('--from requires that only a single package be passed.'); + } + } + + options.from = _formatVersion(options.from); + options.to = _formatVersion(options.to); + const usingYarn = options.packageManager === 'yarn'; + + return async (tree: Tree, context: SchematicContext) => { + const logger = context.logger; + const npmDeps = new Map( + _getAllDependencies(tree).filter(([name, specifier]) => { + try { + return isPkgFromRegistry(name, specifier); + } catch { + logger.warn(`Package ${name} was not found on the registry. Skipping.`); + + return false; + } + }), + ); + const packages = _buildPackageList(options, npmDeps, logger); + + // Grab all package.json from the npm repository. This requires a lot of HTTP calls so we + // try to parallelize as many as possible. + const allPackageMetadata = await Promise.all( + Array.from(npmDeps.keys()).map((depName) => + getNpmPackageJson(depName, logger, { + registry: options.registry, + usingYarn, + verbose: options.verbose, + }), + ), + ); + + // Build a map of all dependencies and their packageJson. + const npmPackageJsonMap = allPackageMetadata.reduce((acc, npmPackageJson) => { + // If the package was not found on the registry. It could be private, so we will just + // ignore. If the package was part of the list, we will error out, but will simply ignore + // if it's either not requested (so just part of package.json. silently). + if (!npmPackageJson.name) { + if (npmPackageJson.requestedName && packages.has(npmPackageJson.requestedName)) { + throw new SchematicsException( + `Package ${JSON.stringify(npmPackageJson.requestedName)} was not found on the ` + + 'registry. Cannot continue as this may be an error.', + ); + } + } else { + // If a name is present, it is assumed to be fully populated + acc.set(npmPackageJson.name, npmPackageJson as NpmRepositoryPackageJson); + } + + return acc; + }, new Map()); + + // Augment the command line package list with packageGroups and forward peer dependencies. + // Each added package may uncover new package groups and peer dependencies, so we must + // repeat this process until the package list stabilizes. + let lastPackagesSize; + do { + lastPackagesSize = packages.size; + npmPackageJsonMap.forEach((npmPackageJson) => { + _addPackageGroup(tree, packages, npmDeps, npmPackageJson, logger); + _addPeerDependencies(tree, packages, npmDeps, npmPackageJson, npmPackageJsonMap, logger); + }); + } while (packages.size > lastPackagesSize); + + // Build the PackageInfo for each module. + const packageInfoMap = new Map(); + npmPackageJsonMap.forEach((npmPackageJson) => { + packageInfoMap.set( + npmPackageJson.name, + _buildPackageInfo(tree, packages, npmDeps, npmPackageJson, logger), + ); + }); + + // Now that we have all the information, check the flags. + if (packages.size > 0) { + if (options.migrateOnly && options.from && options.packages) { + return; + } + + const sublog = new logging.LevelCapLogger('validation', logger.createChild(''), 'warn'); + _validateUpdatePackages(packageInfoMap, !!options.force, !!options.next, sublog); + + _performUpdate(tree, context, packageInfoMap, logger, !!options.migrateOnly); + } else { + _usageMessage(options, packageInfoMap, logger); + } + }; +} diff --git a/packages/angular/cli/src/commands/update/schematic/index_spec.ts b/packages/angular/cli/src/commands/update/schematic/index_spec.ts new file mode 100644 index 000000000000..3954e3c78254 --- /dev/null +++ b/packages/angular/cli/src/commands/update/schematic/index_spec.ts @@ -0,0 +1,338 @@ +/** + * @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 { normalize, virtualFs } from '@angular-devkit/core'; +import { HostTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import * as semver from 'semver'; +import { angularMajorCompatGuarantee } from './index'; + +describe('angularMajorCompatGuarantee', () => { + [ + '5.0.0', + '5.1.0', + '5.20.0', + '6.0.0', + '6.0.0-rc.0', + '6.0.0-beta.0', + '6.1.0-beta.0', + '6.1.0-rc.0', + '6.10.11', + ].forEach((golden) => { + it('works with ' + JSON.stringify(golden), () => { + expect(semver.satisfies(golden, angularMajorCompatGuarantee('^5.0.0'))).toBeTruthy(); + }); + }); +}); + +describe('@schematics/update', () => { + const schematicRunner = new SchematicTestRunner( + '@schematics/update', + require.resolve('./collection.json'), + ); + let host: virtualFs.test.TestHost; + let appTree: UnitTestTree = new UnitTestTree(new HostTree()); + + beforeEach(() => { + host = new virtualFs.test.TestHost({ + '/package.json': `{ + "name": "blah", + "dependencies": { + "@angular-devkit-tests/update-base": "1.0.0" + } + }`, + }); + appTree = new UnitTestTree(new HostTree(host)); + }); + + it('ignores dependencies not hosted on the NPM registry', async () => { + let newTree = new UnitTestTree( + new HostTree( + new virtualFs.test.TestHost({ + '/package.json': `{ + "name": "blah", + "dependencies": { + "@angular-devkit-tests/update-base": "file:update-base-1.0.0.tgz" + } + }`, + }), + ), + ); + + newTree = await schematicRunner.runSchematic('update', undefined, newTree); + const packageJson = JSON.parse(newTree.readContent('/package.json')); + expect(packageJson['dependencies']['@angular-devkit-tests/update-base']).toBe( + 'file:update-base-1.0.0.tgz', + ); + }, 45000); + + it('should not error with yarn 2.0 protocols', async () => { + let newTree = new UnitTestTree( + new HostTree( + new virtualFs.test.TestHost({ + '/package.json': `{ + "name": "blah", + "dependencies": { + "src": "src@link:./src", + "@angular-devkit-tests/update-base": "1.0.0" + } + }`, + }), + ), + ); + + newTree = await schematicRunner.runSchematic( + 'update', + { + packages: ['@angular-devkit-tests/update-base'], + }, + newTree, + ); + const { dependencies } = JSON.parse(newTree.readContent('/package.json')); + expect(dependencies['@angular-devkit-tests/update-base']).toBe('1.1.0'); + }); + + it('updates Angular as compatible with Angular N-1', async () => { + // Add the basic migration package. + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + const dependencies = packageJson['dependencies']; + dependencies['@angular-devkit-tests/update-peer-dependencies-angular-5'] = '1.0.0'; + dependencies['@angular/core'] = '5.1.0'; + dependencies['rxjs'] = '5.5.0'; + dependencies['zone.js'] = '0.8.26'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + const newTree = await schematicRunner.runSchematic( + 'update', + { + packages: ['@angular/core@^6.0.0'], + }, + appTree, + ); + const newPpackageJson = JSON.parse(newTree.readContent('/package.json')); + expect(newPpackageJson['dependencies']['@angular/core'][0]).toBe('6'); + }, 45000); + + it('updates Angular as compatible with Angular N-1 (2)', async () => { + // Add the basic migration package. + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + const dependencies = packageJson['dependencies']; + dependencies['@angular-devkit-tests/update-peer-dependencies-angular-5-2'] = '1.0.0'; + dependencies['@angular/core'] = '5.1.0'; + dependencies['@angular/animations'] = '5.1.0'; + dependencies['@angular/common'] = '5.1.0'; + dependencies['@angular/compiler'] = '5.1.0'; + dependencies['@angular/compiler-cli'] = '5.1.0'; + dependencies['@angular/platform-browser'] = '5.1.0'; + dependencies['rxjs'] = '5.5.0'; + dependencies['zone.js'] = '0.8.26'; + dependencies['typescript'] = '2.4.2'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + const newTree = await schematicRunner.runSchematic( + 'update', + { + packages: ['@angular/core@^6.0.0'], + }, + appTree, + ); + + const newPackageJson = JSON.parse(newTree.readContent('/package.json')); + expect(newPackageJson['dependencies']['@angular/core'][0]).toBe('6'); + expect(newPackageJson['dependencies']['rxjs'][0]).toBe('6'); + expect(newPackageJson['dependencies']['typescript'][0]).toBe('2'); + expect(newPackageJson['dependencies']['typescript'][2]).not.toBe('4'); + }, 45000); + + it('uses packageGroup for versioning', async () => { + // Add the basic migration package. + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + const dependencies = packageJson['dependencies']; + dependencies['@angular-devkit-tests/update-package-group-1'] = '1.0.0'; + dependencies['@angular-devkit-tests/update-package-group-2'] = '1.0.0'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + const newTree = await schematicRunner.runSchematic( + 'update', + { + packages: ['@angular-devkit-tests/update-package-group-1'], + }, + appTree, + ); + const { dependencies: deps } = JSON.parse(newTree.readContent('/package.json')); + expect(deps['@angular-devkit-tests/update-package-group-1']).toBe('1.2.0'); + expect(deps['@angular-devkit-tests/update-package-group-2']).toBe('2.0.0'); + }, 45000); + + it('can migrate only', async () => { + // Add the basic migration package. + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + packageJson['dependencies']['@angular-devkit-tests/update-migrations'] = '1.0.0'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + const newTree = await schematicRunner.runSchematic( + 'update', + { + packages: ['@angular-devkit-tests/update-migrations'], + migrateOnly: true, + }, + appTree, + ); + + const newPackageJson = JSON.parse(newTree.readContent('/package.json')); + expect(newPackageJson['dependencies']['@angular-devkit-tests/update-base']).toBe('1.0.0'); + expect(newPackageJson['dependencies']['@angular-devkit-tests/update-migrations']).toBe('1.0.0'); + }, 45000); + + it('can migrate from only', async () => { + // Add the basic migration package. + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + packageJson['dependencies']['@angular-devkit-tests/update-migrations'] = '1.6.0'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + const newTree = await schematicRunner.runSchematic( + 'update', + { + packages: ['@angular-devkit-tests/update-migrations'], + migrateOnly: true, + from: '0.1.2', + }, + appTree, + ); + const { dependencies } = JSON.parse(newTree.readContent('/package.json')); + expect(dependencies['@angular-devkit-tests/update-migrations']).toBe('1.6.0'); + }, 45000); + + it('can install and migrate with --from (short version number)', async () => { + // Add the basic migration package. + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + packageJson['dependencies']['@angular-devkit-tests/update-migrations'] = '1.6.0'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + const newTree = await schematicRunner.runSchematic( + 'update', + { + packages: ['@angular-devkit-tests/update-migrations'], + migrateOnly: true, + from: '0', + }, + appTree, + ); + const { dependencies } = JSON.parse(newTree.readContent('/package.json')); + expect(dependencies['@angular-devkit-tests/update-migrations']).toBe('1.6.0'); + }, 45000); + + it('validates peer dependencies', async () => { + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + const dependencies = packageJson['dependencies']; + // TODO: when we start using a local npm registry for test packages, add a package that includes + // a optional peer dependency and a non-optional one for this test. Use it instead of + // @angular-devkit/build-angular, whose optional peerdep is @angular/localize and non-optional + // are typescript and @angular/compiler-cli. + dependencies['@angular-devkit/build-angular'] = '0.900.0-next.1'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + const messages: string[] = []; + schematicRunner.logger.subscribe((x) => messages.push(x.message)); + const hasPeerdepMsg = (dep: string) => + messages.some((str) => str.includes(`missing peer dependency of "${dep}"`)); + + await schematicRunner.runSchematic( + 'update', + { + packages: ['@angular-devkit/build-angular'], + next: true, + }, + appTree, + ); + expect(hasPeerdepMsg('@angular/compiler-cli')).toBeTruthy(); + expect(hasPeerdepMsg('typescript')).toBeTruthy(); + expect(hasPeerdepMsg('@angular/localize')).toBeFalsy(); + }, 45000); + + it('does not remove newline at the end of package.json', async () => { + const newlineStyles = ['\n', '\r\n']; + for (const newline of newlineStyles) { + const packageJsonContent = `{ + "name": "blah", + "dependencies": { + "@angular-devkit-tests/update-base": "1.0.0" + } + }${newline}`; + const inputTree = new UnitTestTree( + new HostTree( + new virtualFs.test.TestHost({ + '/package.json': packageJsonContent, + }), + ), + ); + + const resultTree = await schematicRunner.runSchematic( + 'update', + { packages: ['@angular-devkit-tests/update-base'] }, + inputTree, + ); + + const resultTreeContent = resultTree.readContent('/package.json'); + expect(resultTreeContent.endsWith(newline)).toBeTrue(); + } + }); + + it('does not add a newline at the end of package.json', async () => { + const packageJsonContent = `{ + "name": "blah", + "dependencies": { + "@angular-devkit-tests/update-base": "1.0.0" + } + }`; + const inputTree = new UnitTestTree( + new HostTree( + new virtualFs.test.TestHost({ + '/package.json': packageJsonContent, + }), + ), + ); + + const resultTree = await schematicRunner.runSchematic( + 'update', + { packages: ['@angular-devkit-tests/update-base'] }, + inputTree, + ); + + const resultTreeContent = resultTree.readContent('/package.json'); + expect(resultTreeContent.endsWith('}')).toBeTrue(); + }); +}); diff --git a/packages/angular/cli/src/commands/update/schematic/schema.json b/packages/angular/cli/src/commands/update/schematic/schema.json new file mode 100644 index 000000000000..649d2f5db01f --- /dev/null +++ b/packages/angular/cli/src/commands/update/schematic/schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "SchematicsUpdateSchema", + "title": "Schematic Options Schema", + "type": "object", + "properties": { + "packages": { + "description": "The package or packages to update.", + "type": "array", + "items": { + "type": "string" + }, + "$default": { + "$source": "argv" + } + }, + "force": { + "description": "When false (the default), reports an error if installed packages are incompatible with the update.", + "default": false, + "type": "boolean" + }, + "next": { + "description": "Update to the latest version, including beta and RCs.", + "default": false, + "type": "boolean" + }, + "migrateOnly": { + "description": "Perform a migration, but do not update the installed version.", + "default": false, + "type": "boolean" + }, + "from": { + "description": "When using `--migrateOnly` for a single package, the version of that package from which to migrate.", + "type": "string" + }, + "to": { + "description": "When using `--migrateOnly` for a single package, the version of that package to which to migrate.", + "type": "string" + }, + "registry": { + "description": "The npm registry to use.", + "type": "string", + "oneOf": [ + { + "format": "uri" + }, + { + "format": "hostname" + } + ] + }, + "verbose": { + "description": "Display additional details during the update process.", + "type": "boolean" + }, + "packageManager": { + "description": "The preferred package manager configuration files to use for registry settings.", + "type": "string", + "default": "npm", + "enum": ["npm", "yarn", "cnpm", "pnpm", "bun"] + } + }, + "required": [] +} diff --git a/packages/angular/cli/src/commands/version/cli.ts b/packages/angular/cli/src/commands/version/cli.ts new file mode 100644 index 000000000000..4fe53b6596ba --- /dev/null +++ b/packages/angular/cli/src/commands/version/cli.ts @@ -0,0 +1,191 @@ +/** + * @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 nodeModule from 'node:module'; +import { resolve } from 'node:path'; +import { Argv } from 'yargs'; +import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; +import { colors } from '../../utilities/color'; +import { RootCommands } from '../command-config'; + +interface PartialPackageInfo { + name: string; + version: string; + dependencies?: Record; + devDependencies?: Record; +} + +/** + * Major versions of Node.js that are officially supported by Angular. + */ +const SUPPORTED_NODE_MAJORS = [20, 22]; + +const PACKAGE_PATTERNS = [ + /^@angular\/.*/, + /^@angular-devkit\/.*/, + /^@ngtools\/.*/, + /^@schematics\/.*/, + /^rxjs$/, + /^typescript$/, + /^ng-packagr$/, + /^webpack$/, + /^zone\.js$/, +]; + +export default class VersionCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'version'; + aliases = RootCommands['version'].aliases; + describe = 'Outputs Angular CLI version.'; + longDescriptionPath?: string | undefined; + + builder(localYargs: Argv): Argv { + return localYargs; + } + + async run(): Promise { + const { packageManager, logger, root } = this.context; + const localRequire = nodeModule.createRequire(resolve(__filename, '../../../')); + // Trailing slash is used to allow the path to be treated as a directory + const workspaceRequire = nodeModule.createRequire(root + '/'); + + const cliPackage: PartialPackageInfo = localRequire('./package.json'); + let workspacePackage: PartialPackageInfo | undefined; + try { + workspacePackage = workspaceRequire('./package.json'); + } catch {} + + const [nodeMajor] = process.versions.node.split('.').map((part) => Number(part)); + const unsupportedNodeVersion = !SUPPORTED_NODE_MAJORS.includes(nodeMajor); + + const packageNames = new Set( + Object.keys({ + ...cliPackage.dependencies, + ...cliPackage.devDependencies, + ...workspacePackage?.dependencies, + ...workspacePackage?.devDependencies, + }), + ); + + const versions: Record = {}; + for (const name of packageNames) { + if (PACKAGE_PATTERNS.some((p) => p.test(name))) { + versions[name] = this.getVersion(name, workspaceRequire, localRequire); + } + } + + const ngCliVersion = cliPackage.version; + let angularCoreVersion = ''; + const angularSameAsCore: string[] = []; + + if (workspacePackage) { + // Filter all angular versions that are the same as core. + angularCoreVersion = versions['@angular/core']; + if (angularCoreVersion) { + for (const [name, version] of Object.entries(versions)) { + if (version === angularCoreVersion && name.startsWith('@angular/')) { + angularSameAsCore.push(name.replace(/^@angular\//, '')); + delete versions[name]; + } + } + + // Make sure we list them in alphabetical order. + angularSameAsCore.sort(); + } + } + + const namePad = ' '.repeat( + Object.keys(versions).sort((a, b) => b.length - a.length)[0].length + 3, + ); + const asciiArt = ` + _ _ ____ _ ___ + / \\ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _| + / △ \\ | '_ \\ / _\` | | | | |/ _\` | '__| | | | | | | + / ___ \\| | | | (_| | |_| | | (_| | | | |___| |___ | | + /_/ \\_\\_| |_|\\__, |\\__,_|_|\\__,_|_| \\____|_____|___| + |___/ + ` + .split('\n') + .map((x) => colors.red(x)) + .join('\n'); + + logger.info(asciiArt); + logger.info( + ` + Angular CLI: ${ngCliVersion} + Node: ${process.versions.node}${unsupportedNodeVersion ? ' (Unsupported)' : ''} + Package Manager: ${packageManager.name} ${packageManager.version ?? ''} + OS: ${process.platform} ${process.arch} + + Angular: ${angularCoreVersion} + ... ${angularSameAsCore + .reduce((acc, name) => { + // Perform a simple word wrap around 60. + if (acc.length == 0) { + return [name]; + } + const line = acc[acc.length - 1] + ', ' + name; + if (line.length > 60) { + acc.push(name); + } else { + acc[acc.length - 1] = line; + } + + return acc; + }, []) + .join('\n... ')} + + Package${namePad.slice(7)}Version + -------${namePad.replace(/ /g, '-')}------------------ + ${Object.keys(versions) + .map((module) => `${module}${namePad.slice(module.length)}${versions[module]}`) + .sort() + .join('\n')} + `.replace(/^ {6}/gm, ''), + ); + + if (unsupportedNodeVersion) { + logger.warn( + `Warning: The current version of Node (${process.versions.node}) is not supported by Angular.`, + ); + } + } + + private getVersion( + moduleName: string, + workspaceRequire: NodeRequire, + localRequire: NodeRequire, + ): string { + let packageInfo: PartialPackageInfo | undefined; + let cliOnly = false; + + // Try to find the package in the workspace + try { + packageInfo = workspaceRequire(`${moduleName}/package.json`); + } catch {} + + // If not found, try to find within the CLI + if (!packageInfo) { + try { + packageInfo = localRequire(`${moduleName}/package.json`); + cliOnly = true; + } catch {} + } + + // If found, attempt to get the version + if (packageInfo) { + try { + return packageInfo.version + (cliOnly ? ' (cli-only)' : ''); + } catch {} + } + + return ''; + } +} diff --git a/packages/angular/cli/src/typings.ts b/packages/angular/cli/src/typings.ts new file mode 100644 index 000000000000..0ccb3728b882 --- /dev/null +++ b/packages/angular/cli/src/typings.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 + */ + +declare module 'npm-pick-manifest' { + function pickManifest( + metadata: import('./utilities/package-metadata').PackageMetadata, + selector: string, + ): import('./utilities/package-metadata').PackageManifest; + export = pickManifest; +} diff --git a/packages/angular/cli/src/utilities/color.ts b/packages/angular/cli/src/utilities/color.ts new file mode 100644 index 000000000000..3915d99ce248 --- /dev/null +++ b/packages/angular/cli/src/utilities/color.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 { WriteStream } from 'node:tty'; + +export { color as colors, figures } from 'listr2'; + +export function supportColor(stream: NodeJS.WritableStream = process.stdout): boolean { + if (stream instanceof WriteStream) { + return stream.hasColors(); + } + + try { + // The hasColors function does not rely on any instance state and should ideally be static + return WriteStream.prototype.hasColors(); + } catch { + return process.env['FORCE_COLOR'] !== undefined && process.env['FORCE_COLOR'] !== '0'; + } +} diff --git a/packages/angular/cli/src/utilities/completion.ts b/packages/angular/cli/src/utilities/completion.ts new file mode 100644 index 000000000000..436680902395 --- /dev/null +++ b/packages/angular/cli/src/utilities/completion.ts @@ -0,0 +1,306 @@ +/** + * @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 { json, logging } from '@angular-devkit/core'; +import { execFile } from 'node:child_process'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { env } from 'node:process'; +import { colors } from '../utilities/color'; +import { getWorkspace } from '../utilities/config'; +import { forceAutocomplete } from '../utilities/environment-options'; +import { isTTY } from '../utilities/tty'; +import { assertIsError } from './error'; +import { askConfirmation } from './prompt'; + +/** Interface for the autocompletion configuration stored in the global workspace. */ +interface CompletionConfig { + /** + * Whether or not the user has been prompted to set up autocompletion. If `true`, should *not* + * prompt them again. + */ + prompted?: boolean; +} + +/** + * Checks if it is appropriate to prompt the user to setup autocompletion. If not, does nothing. If + * so prompts and sets up autocompletion for the user. Returns an exit code if the program should + * terminate, otherwise returns `undefined`. + * @returns an exit code if the program should terminate, undefined otherwise. + */ +export async function considerSettingUpAutocompletion( + command: string, + logger: logging.Logger, +): Promise { + // Check if we should prompt the user to setup autocompletion. + const completionConfig = await getCompletionConfig(); + if (!(await shouldPromptForAutocompletionSetup(command, completionConfig))) { + return undefined; // Already set up or prompted previously, nothing to do. + } + + // Prompt the user and record their response. + const shouldSetupAutocompletion = await promptForAutocompletion(); + if (!shouldSetupAutocompletion) { + // User rejected the prompt and doesn't want autocompletion. + logger.info( + ` +Ok, you won't be prompted again. Should you change your mind, the following command will set up autocompletion for you: + + ${colors.yellow(`ng completion`)} + `.trim(), + ); + + // Save configuration to remember that the user was prompted and avoid prompting again. + await setCompletionConfig({ ...completionConfig, prompted: true }); + + return undefined; + } + + // User accepted the prompt, set up autocompletion. + let rcFile: string; + try { + rcFile = await initializeAutocomplete(); + } catch (err) { + assertIsError(err); + // Failed to set up autocompeletion, log the error and abort. + logger.error(err.message); + + return 1; + } + + // Notify the user autocompletion was set up successfully. + logger.info( + ` +Appended \`source <(ng completion script)\` to \`${rcFile}\`. Restart your terminal or run the following to autocomplete \`ng\` commands: + + ${colors.yellow(`source <(ng completion script)`)} + `.trim(), + ); + + if (!(await hasGlobalCliInstall())) { + logger.warn( + 'Setup completed successfully, but there does not seem to be a global install of the' + + ' Angular CLI. For autocompletion to work, the CLI will need to be on your `$PATH`, which' + + ' is typically done with the `-g` flag in `npm install -g @angular/cli`.' + + '\n\n' + + 'For more information, see https://angular.dev/cli/completion#global-install', + ); + } + + // Save configuration to remember that the user was prompted. + await setCompletionConfig({ ...completionConfig, prompted: true }); + + return undefined; +} + +async function getCompletionConfig(): Promise { + const wksp = await getWorkspace('global'); + + return wksp?.getCli()?.['completion']; +} + +async function setCompletionConfig(config: CompletionConfig): Promise { + const wksp = await getWorkspace('global'); + if (!wksp) { + throw new Error(`Could not find global workspace`); + } + + wksp.extensions['cli'] ??= {}; + const cli = wksp.extensions['cli']; + if (!json.isJsonObject(cli)) { + throw new Error( + `Invalid config found at ${wksp.filePath}. \`extensions.cli\` should be an object.`, + ); + } + cli.completion = config as json.JsonObject; + await wksp.save(); +} + +async function shouldPromptForAutocompletionSetup( + command: string, + config?: CompletionConfig, +): Promise { + // Force whether or not to prompt for autocomplete to give an easy path for e2e testing to skip. + if (forceAutocomplete !== undefined) { + return forceAutocomplete; + } + + // Don't prompt on `ng update`, 'ng version' or `ng completion`. + if (['version', 'update', 'completion'].includes(command)) { + return false; + } + + // Non-interactive and continuous integration systems don't care about autocompletion. + if (!isTTY()) { + return false; + } + + // Skip prompt if the user has already been prompted. + if (config?.prompted) { + return false; + } + + // `$HOME` variable is necessary to find RC files to modify. + const home = env['HOME']; + if (!home) { + return false; + } + + // Get possible RC files for the current shell. + const shell = env['SHELL']; + if (!shell) { + return false; + } + const rcFiles = getShellRunCommandCandidates(shell, home); + if (!rcFiles) { + return false; // Unknown shell. + } + + // Don't prompt if the user is missing a global CLI install. Autocompletion won't work after setup + // anyway and could be annoying for users running one-off commands via `npx` or using `npm start`. + if ((await hasGlobalCliInstall()) === false) { + return false; + } + + // Check each RC file if they already use `ng completion script` in any capacity and don't prompt. + for (const rcFile of rcFiles) { + const contents = await fs.readFile(rcFile, 'utf-8').catch(() => undefined); + if (contents?.includes('ng completion script')) { + return false; + } + } + + return true; +} + +async function promptForAutocompletion(): Promise { + const autocomplete = await askConfirmation( + ` +Would you like to enable autocompletion? This will set up your terminal so pressing TAB while typing +Angular CLI commands will show possible options and autocomplete arguments. (Enabling autocompletion +will modify configuration files in your home directory.) + ` + .split('\n') + .join(' ') + .trim(), + true, + ); + + return autocomplete; +} + +/** + * Sets up autocompletion for the user's terminal. This attempts to find the configuration file for + * the current shell (`.bashrc`, `.zshrc`, etc.) and append a command which enables autocompletion + * for the Angular CLI. Supports only Bash and Zsh. Returns whether or not it was successful. + * @return The full path of the configuration file modified. + */ +export async function initializeAutocomplete(): Promise { + // Get the currently active `$SHELL` and `$HOME` environment variables. + const shell = env['SHELL']; + if (!shell) { + throw new Error( + '`$SHELL` environment variable not set. Angular CLI autocompletion only supports Bash or' + + " Zsh. If you're on Windows, Cmd and Powershell don't support command autocompletion," + + ' but Git Bash or Windows Subsystem for Linux should work, so please try again in one of' + + ' those environments.', + ); + } + const home = env['HOME']; + if (!home) { + throw new Error( + '`$HOME` environment variable not set. Setting up autocompletion modifies configuration files' + + ' in the home directory and must be set.', + ); + } + + // Get all the files we can add `ng completion` to which apply to the user's `$SHELL`. + const runCommandCandidates = getShellRunCommandCandidates(shell, home); + if (!runCommandCandidates) { + throw new Error( + `Unknown \`$SHELL\` environment variable value (${shell}). Angular CLI autocompletion only supports Bash or Zsh.`, + ); + } + + // Get the first file that already exists or fallback to a new file of the first candidate. + const candidates = await Promise.allSettled( + runCommandCandidates.map((rcFile) => fs.access(rcFile).then(() => rcFile)), + ); + const rcFile = + candidates.find( + (result): result is PromiseFulfilledResult => result.status === 'fulfilled', + )?.value ?? runCommandCandidates[0]; + + // Append Angular autocompletion setup to RC file. + try { + await fs.appendFile( + rcFile, + '\n\n# Load Angular CLI autocompletion.\nsource <(ng completion script)\n', + ); + } catch (err) { + assertIsError(err); + throw new Error(`Failed to append autocompletion setup to \`${rcFile}\`:\n${err.message}`); + } + + return rcFile; +} + +/** Returns an ordered list of possible candidates of RC files used by the given shell. */ +function getShellRunCommandCandidates(shell: string, home: string): string[] | undefined { + if (shell.toLowerCase().includes('bash')) { + return ['.bashrc', '.bash_profile', '.profile'].map((file) => path.join(home, file)); + } else if (shell.toLowerCase().includes('zsh')) { + return ['.zshrc', '.zsh_profile', '.profile'].map((file) => path.join(home, file)); + } else { + return undefined; + } +} + +/** + * Returns whether the user has a global CLI install. + * Execution from `npx` is *not* considered a global CLI install. + * + * This does *not* mean the current execution is from a global CLI install, only that a global + * install exists on the system. + */ +export function hasGlobalCliInstall(): Promise { + // List all binaries with the `ng` name on the user's `$PATH`. + return new Promise((resolve) => { + execFile('which', ['-a', 'ng'], (error, stdout) => { + if (error) { + // No instances of `ng` on the user's `$PATH` + + // `which` returns exit code 2 if an invalid option is specified and `-a` doesn't appear to be + // supported on all systems. Other exit codes mean unknown errors occurred. Can't tell whether + // CLI is globally installed, so treat this as inconclusive. + + // `which` was killed by a signal and did not exit gracefully. Maybe it hung or something else + // went very wrong, so treat this as inconclusive. + resolve(false); + + return; + } + + // Successfully listed all `ng` binaries on the `$PATH`. Look for at least one line which is a + // global install. We can't easily identify global installs, but local installs are typically + // placed in `node_modules/.bin` by NPM / Yarn. `npx` also currently caches files at + // `~/.npm/_npx/*/node_modules/.bin/`, so the same logic applies. + const lines = stdout.split('\n').filter((line) => line !== ''); + const hasGlobalInstall = lines.some((line) => { + // A binary is a local install if it is a direct child of a `node_modules/.bin/` directory. + const parent = path.parse(path.parse(line).dir); + const grandparent = path.parse(parent.dir); + const localInstall = grandparent.base === 'node_modules' && parent.base === '.bin'; + + return !localInstall; + }); + + return resolve(hasGlobalInstall); + }); + }); +} diff --git a/packages/angular/cli/src/utilities/config.ts b/packages/angular/cli/src/utilities/config.ts new file mode 100644 index 000000000000..caa1e2a593f2 --- /dev/null +++ b/packages/angular/cli/src/utilities/config.ts @@ -0,0 +1,420 @@ +/** + * @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 { json, workspaces } from '@angular-devkit/core'; +import { existsSync, promises as fs } from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { PackageManager } from '../../lib/config/workspace-schema'; +import { findUp } from './find-up'; +import { JSONFile, readAndParseJson } from './json-file'; + +function isJsonObject(value: json.JsonValue | undefined): value is json.JsonObject { + return value !== undefined && json.isJsonObject(value); +} + +function createWorkspaceHost(): workspaces.WorkspaceHost { + return { + readFile(path) { + return fs.readFile(path, 'utf-8'); + }, + async writeFile(path, data) { + await fs.writeFile(path, data); + }, + async isDirectory(path) { + try { + const stats = await fs.stat(path); + + return stats.isDirectory(); + } catch { + return false; + } + }, + async isFile(path) { + try { + const stats = await fs.stat(path); + + return stats.isFile(); + } catch { + return false; + } + }, + }; +} + +export const workspaceSchemaPath = path.join(__dirname, '../../lib/config/schema.json'); + +const configNames = ['angular.json', '.angular.json']; +const globalFileName = '.angular-config.json'; +const defaultGlobalFilePath = path.join(os.homedir(), globalFileName); + +function xdgConfigHome(home: string, configFile?: string): string { + // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + const xdgConfigHome = process.env['XDG_CONFIG_HOME'] || path.join(home, '.config'); + const xdgAngularHome = path.join(xdgConfigHome, 'angular'); + + return configFile ? path.join(xdgAngularHome, configFile) : xdgAngularHome; +} + +function xdgConfigHomeOld(home: string): string { + // Check the configuration files in the old location that should be: + // - $XDG_CONFIG_HOME/.angular-config.json (if XDG_CONFIG_HOME is set) + // - $HOME/.config/angular/.angular-config.json (otherwise) + const p = process.env['XDG_CONFIG_HOME'] || path.join(home, '.config', 'angular'); + + return path.join(p, '.angular-config.json'); +} + +function projectFilePath(projectPath?: string): string | null { + // Find the configuration, either where specified, in the Angular CLI project + // (if it's in node_modules) or from the current process. + return ( + (projectPath && findUp(configNames, projectPath)) || + findUp(configNames, process.cwd()) || + findUp(configNames, __dirname) + ); +} + +function globalFilePath(): string | null { + const home = os.homedir(); + if (!home) { + return null; + } + + // follow XDG Base Directory spec + // note that createGlobalSettings() will continue creating + // global file in home directory, with this user will have + // choice to move change its location to meet XDG convention + const xdgConfig = xdgConfigHome(home, 'config.json'); + if (existsSync(xdgConfig)) { + return xdgConfig; + } + // NOTE: This check is for the old configuration location, for more + // information see https://github.com/angular/angular-cli/pull/20556 + const xdgConfigOld = xdgConfigHomeOld(home); + if (existsSync(xdgConfigOld)) { + /* eslint-disable no-console */ + console.warn( + `Old configuration location detected: ${xdgConfigOld}\n` + + `Please move the file to the new location ~/.config/angular/config.json`, + ); + + return xdgConfigOld; + } + + if (existsSync(defaultGlobalFilePath)) { + return defaultGlobalFilePath; + } + + return null; +} + +export class AngularWorkspace { + readonly basePath: string; + + constructor( + private readonly workspace: workspaces.WorkspaceDefinition, + readonly filePath: string, + ) { + this.basePath = path.dirname(filePath); + } + + get extensions(): Record { + return this.workspace.extensions; + } + + get projects(): workspaces.ProjectDefinitionCollection { + return this.workspace.projects; + } + + // Temporary helper functions to support refactoring + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getCli(): Record | undefined { + return this.workspace.extensions['cli'] as Record; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getProjectCli(projectName: string): Record | undefined { + const project = this.workspace.projects.get(projectName); + + return project?.extensions['cli'] as Record; + } + + save(): Promise { + return workspaces.writeWorkspace( + this.workspace, + createWorkspaceHost(), + this.filePath, + workspaces.WorkspaceFormat.JSON, + ); + } + + static async load(workspaceFilePath: string): Promise { + const result = await workspaces.readWorkspace( + workspaceFilePath, + createWorkspaceHost(), + workspaces.WorkspaceFormat.JSON, + ); + + return new AngularWorkspace(result.workspace, workspaceFilePath); + } +} + +const cachedWorkspaces = new Map(); + +export async function getWorkspace(level: 'global'): Promise; +export async function getWorkspace(level: 'local'): Promise; +export async function getWorkspace( + level: 'local' | 'global', +): Promise; + +export async function getWorkspace( + level: 'local' | 'global', +): Promise { + if (cachedWorkspaces.has(level)) { + return cachedWorkspaces.get(level); + } + + const configPath = level === 'local' ? projectFilePath() : globalFilePath(); + if (!configPath) { + if (level === 'global') { + // Unlike a local config, a global config is not mandatory. + // So we create an empty one in memory and keep it as such until it has been modified and saved. + const globalWorkspace = new AngularWorkspace( + { extensions: {}, projects: new workspaces.ProjectDefinitionCollection() }, + defaultGlobalFilePath, + ); + + cachedWorkspaces.set(level, globalWorkspace); + + return globalWorkspace; + } + + cachedWorkspaces.set(level, undefined); + + return undefined; + } + + try { + const workspace = await AngularWorkspace.load(configPath); + cachedWorkspaces.set(level, workspace); + + return workspace; + } catch (error) { + throw new Error( + `Workspace config file cannot be loaded: ${configPath}` + + `\n${error instanceof Error ? error.message : error}`, + ); + } +} + +/** + * This method will load the workspace configuration in raw JSON format. + * When `level` is `global` and file doesn't exists, it will be created. + * + * NB: This method is intended to be used only for `ng config`. + */ +export async function getWorkspaceRaw( + level: 'local' | 'global' = 'local', +): Promise<[JSONFile | null, string | null]> { + let configPath = level === 'local' ? projectFilePath() : globalFilePath(); + + if (!configPath) { + if (level === 'global') { + configPath = defaultGlobalFilePath; + // Config doesn't exist, force create it. + + const globalWorkspace = await getWorkspace('global'); + await globalWorkspace.save(); + } else { + return [null, null]; + } + } + + return [new JSONFile(configPath), configPath]; +} + +export async function validateWorkspace(data: json.JsonObject, isGlobal: boolean): Promise { + const schema = readAndParseJson(workspaceSchemaPath); + + // We should eventually have a dedicated global config schema and use that to validate. + const schemaToValidate: json.schema.JsonSchema = isGlobal + ? { + '$ref': '#/definitions/global', + definitions: schema['definitions'], + } + : schema; + + const { formats } = await import('@angular-devkit/schematics'); + const registry = new json.schema.CoreSchemaRegistry(formats.standardFormats); + const validator = await registry.compile(schemaToValidate); + const { success, errors } = await validator(data); + if (!success) { + throw new json.schema.SchemaValidationException(errors); + } +} + +function findProjectByPath(workspace: AngularWorkspace, location: string): string | null { + const isInside = (base: string, potential: string): boolean => { + const absoluteBase = path.resolve(workspace.basePath, base); + const absolutePotential = path.resolve(workspace.basePath, potential); + const relativePotential = path.relative(absoluteBase, absolutePotential); + if (!relativePotential.startsWith('..') && !path.isAbsolute(relativePotential)) { + return true; + } + + return false; + }; + + const projects = Array.from(workspace.projects) + .map(([name, project]) => [project.root, name] as [string, string]) + .filter((tuple) => isInside(tuple[0], location)) + // Sort tuples by depth, with the deeper ones first. Since the first member is a path and + // we filtered all invalid paths, the longest will be the deepest (and in case of equality + // the sort is stable and the first declared project will win). + .sort((a, b) => b[0].length - a[0].length); + + if (projects.length === 0) { + return null; + } else if (projects.length > 1) { + const found = new Set(); + const sameRoots = projects.filter((v) => { + if (!found.has(v[0])) { + found.add(v[0]); + + return false; + } + + return true; + }); + if (sameRoots.length > 0) { + // Ambiguous location - cannot determine a project + return null; + } + } + + return projects[0][1]; +} + +export function getProjectByCwd(workspace: AngularWorkspace): string | null { + if (workspace.projects.size === 1) { + // If there is only one project, return that one. + return Array.from(workspace.projects.keys())[0]; + } + + const project = findProjectByPath(workspace, process.cwd()); + if (project) { + return project; + } + + return null; +} + +export async function getConfiguredPackageManager(): Promise { + const getPackageManager = (source: json.JsonValue | undefined): PackageManager | null => { + if (isJsonObject(source)) { + const value = source['packageManager']; + if (value && typeof value === 'string') { + return value as PackageManager; + } + } + + return null; + }; + + let result: PackageManager | null = null; + const workspace = await getWorkspace('local'); + if (workspace) { + const project = getProjectByCwd(workspace); + if (project) { + result = getPackageManager(workspace.projects.get(project)?.extensions['cli']); + } + + result ??= getPackageManager(workspace.extensions['cli']); + } + + if (!result) { + const globalOptions = await getWorkspace('global'); + result = getPackageManager(globalOptions?.extensions['cli']); + } + + return result; +} + +export async function getSchematicDefaults( + collection: string, + schematic: string, + project?: string | null, +): Promise<{}> { + const result = {}; + const mergeOptions = (source: json.JsonValue | undefined): void => { + if (isJsonObject(source)) { + // Merge options from the qualified name + Object.assign(result, source[`${collection}:${schematic}`]); + + // Merge options from nested collection schematics + const collectionOptions = source[collection]; + if (isJsonObject(collectionOptions)) { + Object.assign(result, collectionOptions[schematic]); + } + } + }; + + // Global level schematic options + const globalOptions = await getWorkspace('global'); + mergeOptions(globalOptions?.extensions['schematics']); + + const workspace = await getWorkspace('local'); + if (workspace) { + // Workspace level schematic options + mergeOptions(workspace.extensions['schematics']); + + project = project || getProjectByCwd(workspace); + if (project) { + // Project level schematic options + mergeOptions(workspace.projects.get(project)?.extensions['schematics']); + } + } + + return result; +} + +export async function isWarningEnabled(warning: string): Promise { + const getWarning = (source: json.JsonValue | undefined): boolean | undefined => { + if (isJsonObject(source)) { + const warnings = source['warnings']; + if (isJsonObject(warnings)) { + const value = warnings[warning]; + if (typeof value == 'boolean') { + return value; + } + } + } + }; + + let result: boolean | undefined; + + const workspace = await getWorkspace('local'); + if (workspace) { + const project = getProjectByCwd(workspace); + if (project) { + result = getWarning(workspace.projects.get(project)?.extensions['cli']); + } + + result = result ?? getWarning(workspace.extensions['cli']); + } + + if (result === undefined) { + const globalOptions = await getWorkspace('global'); + result = getWarning(globalOptions?.extensions['cli']); + } + + // All warnings are enabled by default + return result ?? true; +} diff --git a/packages/angular/cli/src/utilities/environment-options.ts b/packages/angular/cli/src/utilities/environment-options.ts new file mode 100644 index 000000000000..0f01ce8b09cb --- /dev/null +++ b/packages/angular/cli/src/utilities/environment-options.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 + */ + +function isPresent(variable: string | undefined): variable is string { + return typeof variable === 'string' && variable !== ''; +} + +function isDisabled(variable: string | undefined): boolean { + return isPresent(variable) && (variable === '0' || variable.toLowerCase() === 'false'); +} + +function isEnabled(variable: string | undefined): boolean { + return isPresent(variable) && (variable === '1' || variable.toLowerCase() === 'true'); +} + +function optional(variable: string | undefined): boolean | undefined { + if (!isPresent(variable)) { + return undefined; + } + + return isEnabled(variable); +} + +export const analyticsDisabled = isDisabled(process.env['NG_CLI_ANALYTICS']); +export const isCI = isEnabled(process.env['CI']); +export const disableVersionCheck = isEnabled(process.env['NG_DISABLE_VERSION_CHECK']); +export const ngDebug = isEnabled(process.env['NG_DEBUG']); +export const forceAutocomplete = optional(process.env['NG_FORCE_AUTOCOMPLETE']); diff --git a/packages/angular/cli/src/utilities/eol.ts b/packages/angular/cli/src/utilities/eol.ts new file mode 100644 index 000000000000..02e837649144 --- /dev/null +++ b/packages/angular/cli/src/utilities/eol.ts @@ -0,0 +1,25 @@ +/** + * @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 { EOL } from 'node:os'; + +const CRLF = '\r\n'; +const LF = '\n'; + +export function getEOL(content: string): string { + const newlines = content.match(/(?:\r?\n)/g); + + if (newlines?.length) { + const crlf = newlines.filter((l) => l === CRLF).length; + const lf = newlines.length - crlf; + + return crlf > lf ? CRLF : LF; + } + + return EOL; +} diff --git a/packages/angular/cli/src/utilities/error.ts b/packages/angular/cli/src/utilities/error.ts new file mode 100644 index 000000000000..0ca77c331d2d --- /dev/null +++ b/packages/angular/cli/src/utilities/error.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 assert from 'node:assert'; + +export function assertIsError(value: unknown): asserts value is Error & { code?: string } { + 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); + assert(isError, 'catch clause variable is not an Error instance'); +} diff --git a/packages/angular/cli/src/utilities/find-up.ts b/packages/angular/cli/src/utilities/find-up.ts new file mode 100644 index 000000000000..317c8d8497f5 --- /dev/null +++ b/packages/angular/cli/src/utilities/find-up.ts @@ -0,0 +1,31 @@ +/** + * @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 { existsSync } from 'node:fs'; +import * as path from 'node:path'; + +export function findUp(names: string | string[], from: string) { + 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 (existsSync(p)) { + return p; + } + } + + currentDir = path.dirname(currentDir); + } + + return null; +} diff --git a/packages/angular/cli/src/utilities/json-file.ts b/packages/angular/cli/src/utilities/json-file.ts new file mode 100644 index 000000000000..c0f5fab919e2 --- /dev/null +++ b/packages/angular/cli/src/utilities/json-file.ts @@ -0,0 +1,140 @@ +/** + * @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 { JsonValue } from '@angular-devkit/core'; +import { + Node, + ParseError, + applyEdits, + findNodeAtLocation, + getNodeValue, + modify, + parse, + parseTree, + printParseErrorCode, +} from 'jsonc-parser'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { getEOL } from './eol'; + +export type InsertionIndex = (properties: string[]) => number; +export type JSONPath = (string | number)[]; + +/** @internal */ +export class JSONFile { + content: string; + private eol: string; + + constructor(private readonly path: string) { + const buffer = readFileSync(this.path); + if (buffer) { + this.content = buffer.toString(); + } else { + throw new Error(`Could not read '${path}'.`); + } + + this.eol = getEOL(this.content); + } + + private _jsonAst: Node | undefined; + private get JsonAst(): Node | undefined { + if (this._jsonAst) { + return this._jsonAst; + } + + const errors: ParseError[] = []; + this._jsonAst = parseTree(this.content, errors, { allowTrailingComma: true }); + if (errors.length) { + formatError(this.path, errors); + } + + return this._jsonAst; + } + + get(jsonPath: JSONPath): unknown { + const jsonAstNode = this.JsonAst; + if (!jsonAstNode) { + return undefined; + } + + if (jsonPath.length === 0) { + return getNodeValue(jsonAstNode); + } + + const node = findNodeAtLocation(jsonAstNode, jsonPath); + + return node === undefined ? undefined : getNodeValue(node); + } + + modify( + jsonPath: JSONPath, + value: JsonValue | undefined, + insertInOrder?: InsertionIndex | false, + ): boolean { + if (value === undefined && this.get(jsonPath) === undefined) { + // Cannot remove a value which doesn't exist. + return false; + } + + let getInsertionIndex: InsertionIndex | undefined; + if (insertInOrder === undefined) { + const property = jsonPath.slice(-1)[0]; + getInsertionIndex = (properties) => + [...properties, property].sort().findIndex((p) => p === property); + } else if (insertInOrder !== false) { + getInsertionIndex = insertInOrder; + } + + const edits = modify(this.content, jsonPath, value, { + getInsertionIndex, + // TODO: use indentation from original file. + formattingOptions: { + insertSpaces: true, + tabSize: 2, + eol: this.eol, + }, + }); + + if (edits.length === 0) { + return false; + } + + this.content = applyEdits(this.content, edits); + this._jsonAst = undefined; + + return true; + } + + save(): void { + writeFileSync(this.path, this.content); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function readAndParseJson(path: string): any { + const errors: ParseError[] = []; + const content = parse(readFileSync(path, 'utf-8'), errors, { allowTrailingComma: true }); + if (errors.length) { + formatError(path, errors); + } + + return content; +} + +function formatError(path: string, errors: ParseError[]): never { + const { error, offset } = errors[0]; + throw new Error( + `Failed to parse "${path}" as JSON AST Object. ${printParseErrorCode( + error, + )} at location: ${offset}.`, + ); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function parseJson(content: string): any { + return parse(content, undefined, { allowTrailingComma: true }); +} diff --git a/packages/angular/cli/src/utilities/load-esm.ts b/packages/angular/cli/src/utilities/load-esm.ts new file mode 100644 index 000000000000..6a6220f66288 --- /dev/null +++ b/packages/angular/cli/src/utilities/load-esm.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 + */ + +/** + * Lazily compiled dynamic import loader function. + */ +let load: ((modulePath: string | URL) => Promise) | undefined; + +/** + * 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 function loadEsmModule(modulePath: string | URL): Promise { + load ??= new Function('modulePath', `return import(modulePath);`) as Exclude< + typeof load, + undefined + >; + + return load(modulePath); +} diff --git a/packages/angular/cli/src/utilities/log-file.ts b/packages/angular/cli/src/utilities/log-file.ts new file mode 100644 index 000000000000..1731ca95d4e7 --- /dev/null +++ b/packages/angular/cli/src/utilities/log-file.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 { appendFileSync, mkdtempSync, realpathSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { normalize } from 'node:path'; + +let logPath: string | undefined; + +/** + * Writes an Error to a temporary log file. + * If this method is called multiple times from the same process the same log file will be used. + * @returns The path of the generated log file. + */ +export function writeErrorToLogFile(error: Error): string { + if (!logPath) { + const tempDirectory = mkdtempSync(realpathSync(tmpdir()) + '/ng-'); + logPath = normalize(tempDirectory + '/angular-errors.log'); + } + + appendFileSync(logPath, '[error] ' + (error.stack || error) + '\n\n'); + + return logPath; +} diff --git a/packages/angular/cli/src/utilities/memoize.ts b/packages/angular/cli/src/utilities/memoize.ts new file mode 100644 index 000000000000..2ae55e4b383a --- /dev/null +++ b/packages/angular/cli/src/utilities/memoize.ts @@ -0,0 +1,77 @@ +/** + * @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 decorator that memoizes methods and getters. + * + * **Note**: Be cautious where and how to use this decorator as the size of the cache will grow unbounded. + * + * @see https://en.wikipedia.org/wiki/Memoization + */ +export function memoize( + target: (this: This, ...args: Args) => Return, + context: ClassMemberDecoratorContext, +) { + if (context.kind !== 'method' && context.kind !== 'getter') { + throw new Error('Memoize decorator can only be used on methods or get accessors.'); + } + + const cache = new Map(); + + return function (this: This, ...args: Args): Return { + for (const arg of args) { + if (!isJSONSerializable(arg)) { + throw new Error( + `Argument ${isNonPrimitive(arg) ? arg.toString() : arg} is JSON serializable.`, + ); + } + } + + const key = JSON.stringify(args); + if (cache.has(key)) { + return cache.get(key) as Return; + } + + const result = target.apply(this, args); + cache.set(key, result); + + return result; + }; +} + +/** Method to check if value is a non primitive. */ +function isNonPrimitive(value: unknown): value is object | Function | symbol { + return ( + (value !== null && typeof value === 'object') || + typeof value === 'function' || + typeof value === 'symbol' + ); +} + +/** Method to check if the values are JSON serializable */ +function isJSONSerializable(value: unknown): boolean { + if (!isNonPrimitive(value)) { + // Can be seralized since it's a primitive. + return true; + } + + let nestedValues: unknown[] | undefined; + if (Array.isArray(value)) { + // It's an array, check each item. + nestedValues = value; + } else if (Object.prototype.toString.call(value) === '[object Object]') { + // It's a plain object, check each value. + nestedValues = Object.values(value); + } + + if (!nestedValues || nestedValues.some((v) => !isJSONSerializable(v))) { + return false; + } + + return true; +} diff --git a/packages/angular/cli/src/utilities/memoize_spec.ts b/packages/angular/cli/src/utilities/memoize_spec.ts new file mode 100644 index 000000000000..1c65340764e9 --- /dev/null +++ b/packages/angular/cli/src/utilities/memoize_spec.ts @@ -0,0 +1,160 @@ +/** + * @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 { memoize } from './memoize'; + +describe('memoize', () => { + class Dummy { + @memoize + get random(): number { + return Math.random(); + } + + @memoize + getRandom(_parameter?: unknown): number { + return Math.random(); + } + + @memoize + async getRandomAsync(): Promise { + return Math.random(); + } + } + + it('should call method once', () => { + const dummy = new Dummy(); + const val1 = dummy.getRandom(); + const val2 = dummy.getRandom(); + + // Should return same value since memoized + expect(val1).toBe(val2); + }); + + it('should call method once (async)', async () => { + const dummy = new Dummy(); + const [val1, val2] = await Promise.all([dummy.getRandomAsync(), dummy.getRandomAsync()]); + + // Should return same value since memoized + expect(val1).toBe(val2); + }); + + it('should call getter once', () => { + const dummy = new Dummy(); + const val1 = dummy.random; + const val2 = dummy.random; + + // Should return same value since memoized + expect(val2).toBe(val1); + }); + + it('should call method when parameter changes', () => { + const dummy = new Dummy(); + const val1 = dummy.getRandom(1); + const val2 = dummy.getRandom(2); + const val3 = dummy.getRandom(1); + const val4 = dummy.getRandom(2); + + // Should return same value since memoized + expect(val1).not.toBe(val2); + expect(val1).toBe(val3); + expect(val2).toBe(val4); + }); + + it('should error when used on non getters and methods', () => { + const test = () => { + class DummyError { + @memoize + set random(_value: number) {} + } + + return new DummyError(); + }; + + expect(test).toThrowError('Memoize decorator can only be used on methods or get accessors.'); + }); + + describe('validate method arguments', () => { + it('should error when using Map', () => { + const test = () => new Dummy().getRandom(new Map()); + + expect(test).toThrowError(/Argument \[object Map\] is JSON serializable./); + }); + + it('should error when using Symbol', () => { + const test = () => new Dummy().getRandom(Symbol('')); + + expect(test).toThrowError(/Argument Symbol\(\) is JSON serializable/); + }); + + it('should error when using Function', () => { + const test = () => new Dummy().getRandom(function () {}); + + expect(test).toThrowError(/Argument function \(\) { } is JSON serializable/); + }); + + it('should error when using Map in an array', () => { + const test = () => new Dummy().getRandom([new Map(), true]); + + expect(test).toThrowError(/Argument \[object Map\],true is JSON serializable/); + }); + + it('should error when using Map in an Object', () => { + const test = () => new Dummy().getRandom({ foo: true, prop: new Map() }); + + expect(test).toThrowError(/Argument \[object Object\] is JSON serializable/); + }); + + it('should error when using Function in an Object', () => { + const test = () => new Dummy().getRandom({ foo: true, prop: function () {} }); + + expect(test).toThrowError(/Argument \[object Object\] is JSON serializable/); + }); + + it('should not error when using primitive values in an array', () => { + const test = () => new Dummy().getRandom([1, true, ['foo']]); + + expect(test).not.toThrow(); + }); + + it('should not error when using primitive values in an Object', () => { + const test = () => new Dummy().getRandom({ foo: true, prop: [1, true] }); + + expect(test).not.toThrow(); + }); + + it('should not error when using Boolean', () => { + const test = () => new Dummy().getRandom(true); + + expect(test).not.toThrow(); + }); + + it('should not error when using String', () => { + const test = () => new Dummy().getRandom('foo'); + + expect(test).not.toThrow(); + }); + + it('should not error when using Number', () => { + const test = () => new Dummy().getRandom(1); + + expect(test).not.toThrow(); + }); + + it('should not error when using null', () => { + const test = () => new Dummy().getRandom(null); + + expect(test).not.toThrow(); + }); + + it('should not error when using undefined', () => { + const test = () => new Dummy().getRandom(undefined); + + expect(test).not.toThrow(); + }); + }); +}); diff --git a/packages/angular/cli/src/utilities/package-manager.ts b/packages/angular/cli/src/utilities/package-manager.ts new file mode 100644 index 000000000000..1e249a4f13fa --- /dev/null +++ b/packages/angular/cli/src/utilities/package-manager.ts @@ -0,0 +1,318 @@ +/** + * @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 { isJsonObject, json } from '@angular-devkit/core'; +import { execSync, spawn } from 'node:child_process'; +import { existsSync, promises as fs, realpathSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { PackageManager } from '../../lib/config/workspace-schema'; +import { AngularWorkspace, getProjectByCwd } from './config'; +import { memoize } from './memoize'; + +interface PackageManagerOptions { + saveDev: string; + install: string; + installAll?: string; + prefix: string; + noLockfile: string; +} + +export interface PackageManagerUtilsContext { + globalConfiguration: AngularWorkspace; + workspace?: AngularWorkspace; + root: string; +} + +export class PackageManagerUtils { + constructor(private readonly context: PackageManagerUtilsContext) {} + + /** Get the package manager name. */ + get name(): PackageManager { + return this.getName(); + } + + /** Get the package manager version. */ + get version(): string | undefined { + return this.getVersion(this.name); + } + + /** Install a single package. */ + async install( + packageName: string, + save: 'dependencies' | 'devDependencies' | true = true, + extraArgs: string[] = [], + cwd?: string, + ): Promise { + const packageManagerArgs = this.getArguments(); + const installArgs: string[] = [packageManagerArgs.install, packageName]; + + if (save === 'devDependencies') { + installArgs.push(packageManagerArgs.saveDev); + } + + return this.run([...installArgs, ...extraArgs], { cwd, silent: true }); + } + + /** Install all packages. */ + async installAll(extraArgs: string[] = [], cwd?: string): Promise { + const packageManagerArgs = this.getArguments(); + const installArgs: string[] = []; + if (packageManagerArgs.installAll) { + installArgs.push(packageManagerArgs.installAll); + } + + return this.run([...installArgs, ...extraArgs], { cwd, silent: true }); + } + + /** Install a single package temporary. */ + async installTemp( + packageName: string, + extraArgs?: string[], + ): Promise<{ + success: boolean; + tempNodeModules: string; + }> { + const tempPath = await fs.mkdtemp(join(realpathSync(tmpdir()), 'angular-cli-packages-')); + + // clean up temp directory on process exit + process.on('exit', () => { + try { + rmSync(tempPath, { recursive: true, maxRetries: 3 }); + } catch {} + }); + + // NPM will warn when a `package.json` is not found in the install directory + // Example: + // npm WARN enoent ENOENT: no such file or directory, open '/tmp/.ng-temp-packages-84Qi7y/package.json' + // npm WARN .ng-temp-packages-84Qi7y No description + // npm WARN .ng-temp-packages-84Qi7y No repository field. + // npm WARN .ng-temp-packages-84Qi7y No license field. + + // While we can use `npm init -y` we will end up needing to update the 'package.json' anyways + // because of missing fields. + await fs.writeFile( + join(tempPath, 'package.json'), + JSON.stringify({ + name: 'temp-cli-install', + description: 'temp-cli-install', + repository: 'temp-cli-install', + license: 'MIT', + }), + ); + + // setup prefix/global modules path + const packageManagerArgs = this.getArguments(); + const tempNodeModules = join(tempPath, 'node_modules'); + // Yarn will not append 'node_modules' to the path + const prefixPath = this.name === PackageManager.Yarn ? tempNodeModules : tempPath; + const installArgs: string[] = [ + ...(extraArgs ?? []), + `${packageManagerArgs.prefix}="${prefixPath}"`, + packageManagerArgs.noLockfile, + ]; + + return { + success: await this.install(packageName, true, installArgs, tempPath), + tempNodeModules, + }; + } + + private getArguments(): PackageManagerOptions { + switch (this.name) { + case PackageManager.Yarn: + return { + saveDev: '--dev', + install: 'add', + prefix: '--modules-folder', + noLockfile: '--no-lockfile', + }; + case PackageManager.Pnpm: + return { + saveDev: '--save-dev', + install: 'add', + installAll: 'install', + prefix: '--prefix', + noLockfile: '--no-lockfile', + }; + case PackageManager.Bun: + return { + saveDev: '--development', + install: 'add', + installAll: 'install', + prefix: '--cwd', + noLockfile: '', + }; + default: + return { + saveDev: '--save-dev', + install: 'install', + installAll: 'install', + prefix: '--prefix', + noLockfile: '--no-package-lock', + }; + } + } + + private async run( + args: string[], + options: { cwd?: string; silent?: boolean } = {}, + ): Promise { + const { cwd = process.cwd(), silent = false } = options; + + return new Promise((resolve) => { + const bufferedOutput: { stream: NodeJS.WriteStream; data: Buffer }[] = []; + + const childProcess = spawn(this.name, args, { + // Always pipe stderr to allow for failures to be reported + stdio: silent ? ['ignore', 'ignore', 'pipe'] : 'pipe', + shell: true, + cwd, + }).on('close', (code: number) => { + if (code === 0) { + resolve(true); + } else { + bufferedOutput.forEach(({ stream, data }) => stream.write(data)); + resolve(false); + } + }); + + childProcess.stdout?.on('data', (data: Buffer) => + bufferedOutput.push({ stream: process.stdout, data: data }), + ); + childProcess.stderr?.on('data', (data: Buffer) => + bufferedOutput.push({ stream: process.stderr, data: data }), + ); + }); + } + + @memoize + private getVersion(name: PackageManager): string | undefined { + try { + return execSync(`${name} --version`, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + env: { + ...process.env, + // NPM updater notifier will prevents the child process from closing until it timeout after 3 minutes. + NO_UPDATE_NOTIFIER: '1', + NPM_CONFIG_UPDATE_NOTIFIER: 'false', + }, + }).trim(); + } catch { + return undefined; + } + } + + @memoize + private getName(): PackageManager { + const packageManager = this.getConfiguredPackageManager(); + if (packageManager) { + return packageManager; + } + + const hasNpmLock = this.hasLockfile(PackageManager.Npm); + const hasYarnLock = this.hasLockfile(PackageManager.Yarn); + const hasPnpmLock = this.hasLockfile(PackageManager.Pnpm); + const hasBunLock = this.hasLockfile(PackageManager.Bun); + + // PERF NOTE: `this.getVersion` spawns the package a the child_process which can take around ~300ms at times. + // Therefore, we should only call this method when needed. IE: don't call `this.getVersion(PackageManager.Pnpm)` unless truly needed. + // The result of this method is not stored in a variable because it's memoized. + + if (hasNpmLock) { + // Has NPM lock file. + if (!hasYarnLock && !hasPnpmLock && !hasBunLock && this.getVersion(PackageManager.Npm)) { + // Only NPM lock file and NPM binary is available. + return PackageManager.Npm; + } + } else { + // No NPM lock file. + if (hasYarnLock && this.getVersion(PackageManager.Yarn)) { + // Yarn lock file and Yarn binary is available. + return PackageManager.Yarn; + } else if (hasPnpmLock && this.getVersion(PackageManager.Pnpm)) { + // PNPM lock file and PNPM binary is available. + return PackageManager.Pnpm; + } else if (hasBunLock && this.getVersion(PackageManager.Bun)) { + // Bun lock file and Bun binary is available. + return PackageManager.Bun; + } + } + + if (!this.getVersion(PackageManager.Npm)) { + // Doesn't have NPM installed. + const hasYarn = !!this.getVersion(PackageManager.Yarn); + const hasPnpm = !!this.getVersion(PackageManager.Pnpm); + const hasBun = !!this.getVersion(PackageManager.Bun); + + if (hasYarn && !hasPnpm && !hasBun) { + return PackageManager.Yarn; + } else if (hasPnpm && !hasYarn && !hasBun) { + return PackageManager.Pnpm; + } else if (hasBun && !hasYarn && !hasPnpm) { + return PackageManager.Bun; + } + } + + // TODO: This should eventually inform the user of ambiguous package manager usage. + // Potentially with a prompt to choose and optionally set as the default. + return PackageManager.Npm; + } + + private hasLockfile(packageManager: PackageManager): boolean { + let lockfileName: string; + switch (packageManager) { + case PackageManager.Yarn: + lockfileName = 'yarn.lock'; + break; + case PackageManager.Pnpm: + lockfileName = 'pnpm-lock.yaml'; + break; + case PackageManager.Bun: + lockfileName = 'bun.lockb'; + break; + case PackageManager.Npm: + default: + lockfileName = 'package-lock.json'; + break; + } + + return existsSync(join(this.context.root, lockfileName)); + } + + private getConfiguredPackageManager(): PackageManager | undefined { + const getPackageManager = (source: json.JsonValue | undefined): PackageManager | undefined => { + if (source && isJsonObject(source)) { + const value = source['packageManager']; + if (typeof value === 'string') { + return value as PackageManager; + } + } + + return undefined; + }; + + let result: PackageManager | undefined; + const { workspace: localWorkspace, globalConfiguration: globalWorkspace } = this.context; + if (localWorkspace) { + const project = getProjectByCwd(localWorkspace); + if (project) { + result = getPackageManager(localWorkspace.projects.get(project)?.extensions['cli']); + } + + result ??= getPackageManager(localWorkspace.extensions['cli']); + } + + if (!result) { + result = getPackageManager(globalWorkspace.extensions['cli']); + } + + return result; + } +} diff --git a/packages/angular/cli/src/utilities/package-metadata.ts b/packages/angular/cli/src/utilities/package-metadata.ts new file mode 100644 index 000000000000..7aa0cb71c8ce --- /dev/null +++ b/packages/angular/cli/src/utilities/package-metadata.ts @@ -0,0 +1,332 @@ +/** + * @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 * as lockfile from '@yarnpkg/lockfile'; +import * as ini from 'ini'; +import { existsSync, readFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import * as path from 'node:path'; +import type { Manifest, Packument } from 'pacote'; + +export interface PackageMetadata extends Packument, NgPackageManifestProperties { + tags: Record; + versions: Record; +} + +export interface NpmRepositoryPackageJson extends PackageMetadata { + requestedName?: string; +} + +export type NgAddSaveDependency = 'dependencies' | 'devDependencies' | boolean; + +export interface PackageIdentifier { + type: 'git' | 'tag' | 'version' | 'range' | 'file' | 'directory' | 'remote'; + name: string; + scope: string | null; + registry: boolean; + raw: string; + fetchSpec: string; + rawSpec: string; +} + +export interface NgPackageManifestProperties { + 'ng-add'?: { + save?: NgAddSaveDependency; + }; + 'ng-update'?: { + migrations?: string; + packageGroup?: string[] | Record; + packageGroupName?: string; + requirements?: string[] | Record; + }; +} + +export interface PackageManifest extends Manifest, NgPackageManifestProperties { + deprecated?: boolean; +} + +interface PackageManagerOptions extends Record { + forceAuth?: Record; +} + +let npmrc: PackageManagerOptions; +const npmPackageJsonCache = new Map>>(); + +function ensureNpmrc(logger: logging.LoggerApi, usingYarn: boolean, verbose: boolean): void { + if (!npmrc) { + try { + npmrc = readOptions(logger, false, verbose); + } catch {} + + if (usingYarn) { + try { + npmrc = { ...npmrc, ...readOptions(logger, true, verbose) }; + } catch {} + } + } +} + +function readOptions( + logger: logging.LoggerApi, + yarn = false, + showPotentials = false, +): PackageManagerOptions { + const cwd = process.cwd(); + const baseFilename = yarn ? 'yarnrc' : 'npmrc'; + const dotFilename = '.' + baseFilename; + + let globalPrefix: string; + if (process.env.PREFIX) { + globalPrefix = process.env.PREFIX; + } else { + globalPrefix = path.dirname(process.execPath); + if (process.platform !== 'win32') { + globalPrefix = path.dirname(globalPrefix); + } + } + + const defaultConfigLocations = [ + (!yarn && process.env.NPM_CONFIG_GLOBALCONFIG) || path.join(globalPrefix, 'etc', baseFilename), + (!yarn && process.env.NPM_CONFIG_USERCONFIG) || path.join(homedir(), dotFilename), + ]; + + const projectConfigLocations: string[] = [path.join(cwd, dotFilename)]; + if (yarn) { + const root = path.parse(cwd).root; + for (let curDir = path.dirname(cwd); curDir && curDir !== root; curDir = path.dirname(curDir)) { + projectConfigLocations.unshift(path.join(curDir, dotFilename)); + } + } + + if (showPotentials) { + logger.info(`Locating potential ${baseFilename} files:`); + } + + let rcOptions: PackageManagerOptions = {}; + for (const location of [...defaultConfigLocations, ...projectConfigLocations]) { + if (existsSync(location)) { + if (showPotentials) { + logger.info(`Trying '${location}'...found.`); + } + + const data = readFileSync(location, 'utf8'); + // Normalize RC options that are needed by 'npm-registry-fetch'. + // See: https://github.com/npm/npm-registry-fetch/blob/ebddbe78a5f67118c1f7af2e02c8a22bcaf9e850/index.js#L99-L126 + const rcConfig: PackageManagerOptions = yarn ? lockfile.parse(data) : ini.parse(data); + + rcOptions = normalizeOptions(rcConfig, location, rcOptions); + } + } + + const envVariablesOptions: PackageManagerOptions = {}; + for (const [key, value] of Object.entries(process.env)) { + if (!value) { + continue; + } + + let normalizedName = key.toLowerCase(); + if (normalizedName.startsWith('npm_config_')) { + normalizedName = normalizedName.substring(11); + } else if (yarn && normalizedName.startsWith('yarn_')) { + normalizedName = normalizedName.substring(5); + } else { + continue; + } + + if ( + normalizedName === 'registry' && + rcOptions['registry'] && + value === 'https://registry.yarnpkg.com/' && + process.env['npm_config_user_agent']?.includes('yarn') + ) { + // When running `ng update` using yarn (`yarn ng update`), yarn will set the `npm_config_registry` env variable to `https://registry.yarnpkg.com` + // even when an RC file is present with a different repository. + // This causes the registry specified in the RC to always be overridden with the below logic. + continue; + } + + normalizedName = normalizedName.replace(/(?!^)_/g, '-'); // don't replace _ at the start of the key.s + envVariablesOptions[normalizedName] = value; + } + + return normalizeOptions(envVariablesOptions, undefined, rcOptions); +} + +function normalizeOptions( + rawOptions: PackageManagerOptions, + location = process.cwd(), + existingNormalizedOptions: PackageManagerOptions = {}, +): PackageManagerOptions { + const options = { ...existingNormalizedOptions }; + + for (const [key, value] of Object.entries(rawOptions)) { + let substitutedValue = value; + + // Substitute any environment variable references. + if (typeof value === 'string') { + substitutedValue = value.replace(/\$\{([^}]+)\}/, (_, name) => process.env[name] || ''); + } + + switch (key) { + // Unless auth options are scope with the registry url it appears that npm-registry-fetch ignores them, + // even though they are documented. + // https://github.com/npm/npm-registry-fetch/blob/8954f61d8d703e5eb7f3d93c9b40488f8b1b62ac/README.md + // https://github.com/npm/npm-registry-fetch/blob/8954f61d8d703e5eb7f3d93c9b40488f8b1b62ac/auth.js#L45-L91 + case '_authToken': + case 'token': + case 'username': + case 'password': + case '_auth': + case 'auth': + options['forceAuth'] ??= {}; + options['forceAuth'][key] = substitutedValue; + break; + case 'noproxy': + case 'no-proxy': + options['noProxy'] = substitutedValue; + break; + case 'maxsockets': + options['maxSockets'] = substitutedValue; + break; + case 'https-proxy': + case 'proxy': + options['proxy'] = substitutedValue; + break; + case 'strict-ssl': + options['strictSSL'] = substitutedValue; + break; + case 'local-address': + options['localAddress'] = substitutedValue; + break; + case 'cafile': + if (typeof substitutedValue === 'string') { + const cafile = path.resolve(path.dirname(location), substitutedValue); + try { + options['ca'] = readFileSync(cafile, 'utf8').replace(/\r?\n/g, '\n'); + } catch {} + } + break; + case 'before': + options['before'] = + typeof substitutedValue === 'string' ? new Date(substitutedValue) : substitutedValue; + break; + default: + options[key] = substitutedValue; + break; + } + } + + return options; +} + +export async function fetchPackageMetadata( + name: string, + logger: logging.LoggerApi, + options?: { + registry?: string; + usingYarn?: boolean; + verbose?: boolean; + }, +): Promise { + const { usingYarn, verbose, registry } = { + registry: undefined, + usingYarn: false, + verbose: false, + ...options, + }; + + ensureNpmrc(logger, usingYarn, verbose); + const { packument } = await import('pacote'); + const response = await packument(name, { + fullMetadata: true, + ...npmrc, + ...(registry ? { registry } : {}), + }); + + if (!response.versions) { + // While pacote type declares that versions cannot be undefined this is not the case. + response.versions = {}; + } + + // Normalize the response + const metadata: PackageMetadata = { + ...response, + tags: {}, + }; + + if (response['dist-tags']) { + for (const [tag, version] of Object.entries(response['dist-tags'])) { + const manifest = metadata.versions[version]; + if (manifest) { + metadata.tags[tag] = manifest; + } else if (verbose) { + logger.warn(`Package ${metadata.name} has invalid version metadata for '${tag}'.`); + } + } + } + + return metadata; +} + +export async function fetchPackageManifest( + name: string, + logger: logging.LoggerApi, + options: { + registry?: string; + usingYarn?: boolean; + verbose?: boolean; + } = {}, +): Promise { + const { usingYarn = false, verbose = false, registry } = options; + ensureNpmrc(logger, usingYarn, verbose); + const { manifest } = await import('pacote'); + + const response = await manifest(name, { + fullMetadata: true, + ...npmrc, + ...(registry ? { registry } : {}), + }); + + return response; +} + +export async function getNpmPackageJson( + packageName: string, + logger: logging.LoggerApi, + options: { + registry?: string; + usingYarn?: boolean; + verbose?: boolean; + } = {}, +): Promise> { + const cachedResponse = npmPackageJsonCache.get(packageName); + if (cachedResponse) { + return cachedResponse; + } + + const { usingYarn = false, verbose = false, registry } = options; + ensureNpmrc(logger, usingYarn, verbose); + const { packument } = await import('pacote'); + const response = packument(packageName, { + fullMetadata: true, + ...npmrc, + ...(registry ? { registry } : {}), + }).then((response) => { + // While pacote type declares that versions cannot be undefined this is not the case. + if (!response.versions) { + response.versions = {}; + } + + return response; + }); + + npmPackageJsonCache.set(packageName, response); + + return response; +} diff --git a/packages/angular/cli/src/utilities/package-tree.ts b/packages/angular/cli/src/utilities/package-tree.ts new file mode 100644 index 000000000000..14e8a9edd689 --- /dev/null +++ b/packages/angular/cli/src/utilities/package-tree.ts @@ -0,0 +1,86 @@ +/** + * @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 fs from 'node:fs'; +import { dirname, join } from 'node:path'; +import * as resolve from 'resolve'; +import { NgAddSaveDependency } from './package-metadata'; + +interface PackageJson { + name: string; + version: string; + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; + 'ng-update'?: { + migrations?: string; + }; + 'ng-add'?: { + save?: NgAddSaveDependency; + }; +} + +function getAllDependencies(pkg: PackageJson): Set<[string, string]> { + return new Set([ + ...Object.entries(pkg.dependencies || []), + ...Object.entries(pkg.devDependencies || []), + ...Object.entries(pkg.peerDependencies || []), + ...Object.entries(pkg.optionalDependencies || []), + ]); +} + +export interface PackageTreeNode { + name: string; + version: string; + path: string; + package: PackageJson | undefined; +} + +export async function readPackageJson(packageJsonPath: string): Promise { + try { + return JSON.parse((await fs.promises.readFile(packageJsonPath)).toString()) as PackageJson; + } catch { + return undefined; + } +} + +export function findPackageJson(workspaceDir: string, packageName: string): string | undefined { + try { + // avoid require.resolve here, see: https://github.com/angular/angular-cli/pull/18610#issuecomment-681980185 + const packageJsonPath = resolve.sync(`${packageName}/package.json`, { basedir: workspaceDir }); + + return packageJsonPath; + } catch { + return undefined; + } +} + +export async function getProjectDependencies(dir: string): Promise> { + const pkg = await readPackageJson(join(dir, 'package.json')); + if (!pkg) { + throw new Error('Could not find package.json'); + } + + const results = new Map(); + for (const [name, version] of getAllDependencies(pkg)) { + const packageJsonPath = findPackageJson(dir, name); + if (!packageJsonPath) { + continue; + } + + results.set(name, { + name, + version, + path: dirname(packageJsonPath), + package: await readPackageJson(packageJsonPath), + }); + } + + return results; +} diff --git a/packages/angular/cli/src/utilities/project.ts b/packages/angular/cli/src/utilities/project.ts new file mode 100644 index 000000000000..39ce2e6d3e83 --- /dev/null +++ b/packages/angular/cli/src/utilities/project.ts @@ -0,0 +1,56 @@ +/** + * @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 { normalize } from '@angular-devkit/core'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { findUp } from './find-up'; + +interface PackageDependencies { + dependencies?: Record; + devDependencies?: Record; +} + +export function findWorkspaceFile(currentDirectory = process.cwd()): string | null { + const possibleConfigFiles = ['angular.json', '.angular.json']; + const configFilePath = findUp(possibleConfigFiles, currentDirectory); + if (configFilePath === null) { + return null; + } + + const possibleDir = path.dirname(configFilePath); + + const homedir = os.homedir(); + if (normalize(possibleDir) === normalize(homedir)) { + const packageJsonPath = path.join(possibleDir, 'package.json'); + + try { + const packageJsonText = fs.readFileSync(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(packageJsonText) as PackageDependencies; + if (!containsCliDep(packageJson)) { + // No CLI dependency + return null; + } + } catch { + // No or invalid package.json + return null; + } + } + + return configFilePath; +} + +function containsCliDep(obj?: PackageDependencies): boolean { + const pkgName = '@angular/cli'; + if (!obj) { + return false; + } + + return !!(obj.dependencies?.[pkgName] || obj.devDependencies?.[pkgName]); +} diff --git a/packages/angular/cli/src/utilities/prompt.ts b/packages/angular/cli/src/utilities/prompt.ts new file mode 100644 index 000000000000..c1cd8eba0c3c --- /dev/null +++ b/packages/angular/cli/src/utilities/prompt.ts @@ -0,0 +1,74 @@ +/** + * @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 { isTTY } from './tty'; + +export async function askConfirmation( + message: string, + defaultResponse: boolean, + noTTYResponse?: boolean, +): Promise { + if (!isTTY()) { + return noTTYResponse ?? defaultResponse; + } + + const { confirm } = await import('@inquirer/prompts'); + const answer = await confirm({ + message, + default: defaultResponse, + theme: { + prefix: '', + }, + }); + + return answer; +} + +export async function askQuestion( + message: string, + choices: { name: string; value: string | null }[], + defaultResponseIndex: number, + noTTYResponse: null | string, +): Promise { + if (!isTTY()) { + return noTTYResponse; + } + + const { select } = await import('@inquirer/prompts'); + const answer = await select({ + message, + choices, + default: defaultResponseIndex, + theme: { + prefix: '', + }, + }); + + return answer; +} + +export async function askChoices( + message: string, + choices: { name: string; value: string; checked?: boolean }[], + noTTYResponse: string[] | null, +): Promise { + if (!isTTY()) { + return noTTYResponse; + } + + const { checkbox } = await import('@inquirer/prompts'); + const answers = await checkbox({ + message, + choices, + theme: { + prefix: '', + }, + }); + + return answers; +} diff --git a/packages/angular/cli/src/utilities/tty.ts b/packages/angular/cli/src/utilities/tty.ts new file mode 100644 index 000000000000..db6543926941 --- /dev/null +++ b/packages/angular/cli/src/utilities/tty.ts @@ -0,0 +1,22 @@ +/** + * @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 + */ + +function _isTruthy(value: undefined | string): boolean { + // Returns true if value is a string that is anything but 0 or false. + return value !== undefined && value !== '0' && value.toUpperCase() !== 'FALSE'; +} + +export function isTTY(stream: NodeJS.WriteStream = process.stdout): boolean { + // If we force TTY, we always return true. + const force = process.env['NG_FORCE_TTY']; + if (force !== undefined) { + return _isTruthy(force); + } + + return !!stream.isTTY && !_isTruthy(process.env['CI']); +} diff --git a/packages/angular/cli/src/utilities/version.ts b/packages/angular/cli/src/utilities/version.ts new file mode 100644 index 000000000000..d16feb2d4b15 --- /dev/null +++ b/packages/angular/cli/src/utilities/version.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 + */ + +// Same structure as used in framework packages +class Version { + readonly major: string; + readonly minor: string; + readonly patch: string; + + constructor(readonly full: string) { + const [major, minor, patch] = full.split('-', 1)[0].split('.', 3); + this.major = major; + this.minor = minor; + this.patch = patch; + } +} + +export const VERSION = new Version('0.0.0-PLACEHOLDER'); diff --git a/packages/angular/cli/tasks/npm-install.ts b/packages/angular/cli/tasks/npm-install.ts deleted file mode 100644 index 243587054b88..000000000000 --- a/packages/angular/cli/tasks/npm-install.ts +++ /dev/null @@ -1,80 +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, terminal } from '@angular-devkit/core'; -import { ModuleNotFoundException, resolve } from '@angular-devkit/core/node'; -import { spawn } from 'child_process'; - - -export type NpmInstall = (packageName: string, - logger: logging.Logger, - packageManager: string, - projectRoot: string, - save?: boolean) => Promise; - -export default async function (packageName: string, - logger: logging.Logger, - packageManager: string, - projectRoot: string, - save = true) { - const installArgs: string[] = []; - switch (packageManager) { - case 'cnpm': - case 'npm': - installArgs.push('install', '--quiet'); - break; - - case 'yarn': - installArgs.push('add'); - break; - - default: - packageManager = 'npm'; - installArgs.push('install', '--quiet'); - break; - } - - logger.info(terminal.green(`Installing packages for tooling via ${packageManager}.`)); - - if (packageName) { - try { - // Verify if we need to install the package (it might already be there). - // If it's available and we shouldn't save, simply return. Nothing to be done. - resolve(packageName, { checkLocal: true, basedir: projectRoot }); - - return; - } catch (e) { - if (!(e instanceof ModuleNotFoundException)) { - throw e; - } - } - installArgs.push(packageName); - } - - if (!save) { - installArgs.push('--no-save'); - } - const installOptions = { - stdio: 'inherit', - shell: true, - }; - - await new Promise((resolve, reject) => { - spawn(packageManager, installArgs, installOptions) - .on('close', (code: number) => { - if (code === 0) { - logger.info(terminal.green(`Installed packages for tooling via ${packageManager}.`)); - resolve(); - } else { - const message = 'Package install failed, see above.'; - logger.info(terminal.red(message)); - reject(message); - } - }); - }); -} diff --git a/packages/angular/cli/upgrade/version.ts b/packages/angular/cli/upgrade/version.ts deleted file mode 100644 index 9798dc4aa0ab..000000000000 --- a/packages/angular/cli/upgrade/version.ts +++ /dev/null @@ -1,174 +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 { tags, terminal } from '@angular-devkit/core'; -import * as path from 'path'; -import { SemVer, satisfies } from 'semver'; -import { isWarningEnabled } from '../utilities/config'; -import { requireProjectModule } from '../utilities/require-project-module'; - - -export class Version { - private _semver: SemVer | null = null; - constructor(private _version: string | null = null) { - this._semver = _version ? new SemVer(_version) : null; - } - - isAlpha() { return this.qualifier == 'alpha'; } - isBeta() { return this.qualifier == 'beta'; } - isReleaseCandidate() { return this.qualifier == 'rc'; } - isKnown() { return this._version !== null; } - - isLocal() { return this.isKnown() && this._version && path.isAbsolute(this._version); } - isGreaterThanOrEqualTo(other: SemVer) { - return this._semver !== null && this._semver.compare(other) >= 0; - } - - get major() { return this._semver ? this._semver.major : 0; } - get minor() { return this._semver ? this._semver.minor : 0; } - get patch() { return this._semver ? this._semver.patch : 0; } - get qualifier() { return this._semver ? this._semver.prerelease[0] : ''; } - get extra() { return this._semver ? this._semver.prerelease[1] : ''; } - - toString() { return this._version; } - - static assertCompatibleAngularVersion(projectRoot: string) { - let angularPkgJson; - let rxjsPkgJson; - - const isInside = (base: string, potential: string): boolean => { - const absoluteBase = path.resolve(base); - const absolutePotential = path.resolve(potential); - const relativePotential = path.relative(absoluteBase, absolutePotential); - if (!relativePotential.startsWith('..') && !path.isAbsolute(relativePotential)) { - return true; - } - - return false; - }; - - try { - const resolveOptions = { paths: [ path.join(projectRoot, 'node_modules'), projectRoot ] }; - const angularPackagePath = require.resolve('@angular/core/package.json', resolveOptions); - const rxjsPackagePath = require.resolve('rxjs/package.json', resolveOptions); - - if (!isInside(projectRoot, angularPackagePath) - || !isInside(projectRoot, rxjsPackagePath)) { - throw new Error(); - } - - angularPkgJson = require(angularPackagePath); - rxjsPkgJson = require(rxjsPackagePath); - } catch { - console.error(terminal.bold(terminal.red(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'])) { - console.error(terminal.bold(terminal.red(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); - } - - const angularVersion = new Version(angularPkgJson['version']); - const rxjsVersion = new Version(rxjsPkgJson['version']); - - if (angularVersion.isLocal()) { - console.warn(terminal.yellow('Using a local version of angular. Proceeding with care...')); - - return; - } - - if (!angularVersion.isGreaterThanOrEqualTo(new SemVer('5.0.0'))) { - console.error(terminal.bold(terminal.red(tags.stripIndents` - This version of CLI is only compatible with Angular version 5.0.0 or higher. - - Please visit the link below to find instructions on how to update Angular. - https://angular-update-guide.firebaseapp.com/ - ` + '\n'))); - process.exit(3); - } else if ( - angularVersion.isGreaterThanOrEqualTo(new SemVer('6.0.0-rc.0')) - && !rxjsVersion.isGreaterThanOrEqualTo(new SemVer('5.6.0-forward-compat.0')) - && !rxjsVersion.isGreaterThanOrEqualTo(new SemVer('6.0.0-beta.0')) - ) { - console.error(terminal.bold(terminal.red(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 ( - angularVersion.isGreaterThanOrEqualTo(new SemVer('6.0.0-rc.0')) - && !rxjsVersion.isGreaterThanOrEqualTo(new SemVer('6.0.0-beta.0')) - ) { - console.warn(terminal.bold(terminal.red(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'))); - } - } - - static assertTypescriptVersion(projectRoot: string) { - if (!isWarningEnabled('typescriptMismatch')) { - return; - } - let compilerVersion: string, tsVersion: string; - try { - compilerVersion = requireProjectModule(projectRoot, '@angular/compiler-cli').VERSION.full; - tsVersion = requireProjectModule(projectRoot, 'typescript').version; - } catch { - console.error(terminal.bold(terminal.red(tags.stripIndents` - Versions of @angular/compiler-cli and typescript could not be determined. - The most common reason for this is a broken npm install. - - Please make sure your package.json contains both @angular/compiler-cli and typescript in - devDependencies, then delete node_modules and package-lock.json (if you have one) and - run npm install again. - `))); - process.exit(2); - - return; - } - - const versionCombos = [ - { compiler: '>=2.3.1 <3.0.0', typescript: '>=2.0.2 <2.3.0' }, - { compiler: '>=4.0.0-beta.0 <5.0.0', typescript: '>=2.1.0 <2.4.0' }, - { compiler: '>=5.0.0-beta.0 <5.1.0', typescript: '>=2.4.2 <2.5.0' }, - { compiler: '>=5.1.0-beta.0 <5.2.0', typescript: '>=2.4.2 <2.6.0' }, - { compiler: '>=5.2.0-beta.0 <6.0.0', typescript: '>=2.4.2 <2.7.0' }, - { compiler: '>=6.0.0-beta.0 <7.0.0', typescript: '>=2.7.0 <2.8.0' }, - ]; - - const currentCombo = versionCombos.find((combo) => satisfies(compilerVersion, combo.compiler)); - - if (currentCombo && !satisfies(tsVersion, currentCombo.typescript)) { - // First line of warning looks weird being split in two, disable tslint for it. - console.log((terminal.yellow('\n' + tags.stripIndent` - @angular/compiler-cli@${compilerVersion} requires typescript@'${ - currentCombo.typescript}' but ${tsVersion} was found instead. - Using this version can result in undefined behaviour and difficult to debug problems. - - Please run the following command to install a compatible version of TypeScript. - - npm install typescript@'${currentCombo.typescript}' - - To disable this warning run "ng config cli.warnings.typescriptMismatch false". - ` + '\n'))); - } - } - -} diff --git a/packages/angular/cli/utilities/INITIAL_COMMIT_MESSAGE.txt b/packages/angular/cli/utilities/INITIAL_COMMIT_MESSAGE.txt deleted file mode 100644 index 2f0b94d3b50c..000000000000 --- a/packages/angular/cli/utilities/INITIAL_COMMIT_MESSAGE.txt +++ /dev/null @@ -1,8 +0,0 @@ -chore: initial commit from @angular/cli - - _ _ ____ _ ___ - / \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _| - / △ \ | '_ \ / _\` | | | | |/ _\` | '__| | | | | | | - / ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | | -/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___| - |___/ diff --git a/packages/angular/cli/utilities/check-package-manager.ts b/packages/angular/cli/utilities/check-package-manager.ts deleted file mode 100644 index d8d20aaf9662..000000000000 --- a/packages/angular/cli/utilities/check-package-manager.ts +++ /dev/null @@ -1,53 +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 { terminal } from '@angular-devkit/core'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import { getPackageManager } from './config'; - -const execPromise = promisify(exec); -const packageManager = getPackageManager(); - - -export function checkYarnOrCNPM() { - - // Don't show messages if user has already changed the default. - if (packageManager !== 'default') { - return Promise.resolve(); - } - - return Promise - .all([checkYarn(), checkCNPM()]) - .then((data: Array) => { - const [isYarnInstalled, isCNPMInstalled] = data; - if (isYarnInstalled && isCNPMInstalled) { - console.log(terminal.yellow('You can `ng config -g cli.packageManager yarn` ' - + 'or `ng config -g cli.packageManager cnpm`.')); - } else if (isYarnInstalled) { - console.log(terminal.yellow('You can `ng config -g cli.packageManager yarn`.')); - } else if (isCNPMInstalled) { - console.log(terminal.yellow('You can `ng config -g cli.packageManager cnpm`.')); - } else { - if (packageManager !== 'default' && packageManager !== 'npm') { - console.log(terminal.yellow(`Seems that ${packageManager} is not installed.`)); - console.log(terminal.yellow('You can `ng config -g cli.packageManager npm`.')); - } - } - }); -} - -function checkYarn() { - return execPromise('yarn --version') - .then(() => true, () => false); -} - -function checkCNPM() { - return execPromise('cnpm --version') - .then(() => true, () => false); -} diff --git a/packages/angular/cli/utilities/config.ts b/packages/angular/cli/utilities/config.ts deleted file mode 100644 index d9fdb26e9aaf..000000000000 --- a/packages/angular/cli/utilities/config.ts +++ /dev/null @@ -1,386 +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 { - JsonAstObject, - JsonObject, - JsonParseMode, - experimental, - normalize, - parseJson, - parseJsonAst, - virtualFs, -} from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { existsSync, readFileSync, writeFileSync } from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { findUp } from './find-up'; - -function getSchemaLocation(): string { - return path.join(__dirname, '../lib/config/schema.json'); -} - -export const workspaceSchemaPath = getSchemaLocation(); - -const configNames = [ 'angular.json', '.angular.json' ]; -const globalFileName = '.angular-config.json'; - -function projectFilePath(projectPath?: string): string | null { - // Find the configuration, either where specified, in the Angular CLI project - // (if it's in node_modules) or from the current process. - return (projectPath && findUp(configNames, projectPath)) - || findUp(configNames, process.cwd()) - || findUp(configNames, __dirname); -} - -function globalFilePath(): string | null { - const home = os.homedir(); - if (!home) { - return null; - } - - const p = path.join(home, globalFileName); - if (existsSync(p)) { - return p; - } - - return null; -} - -const cachedWorkspaces = new Map(); - -export function getWorkspace( - level: 'local' | 'global' = 'local', -): experimental.workspace.Workspace | null { - const cached = cachedWorkspaces.get(level); - if (cached != undefined) { - return cached; - } - - const configPath = level === 'local' ? projectFilePath() : globalFilePath(); - - if (!configPath) { - cachedWorkspaces.set(level, null); - - return null; - } - - const root = normalize(path.dirname(configPath)); - const file = normalize(path.basename(configPath)); - const workspace = new experimental.workspace.Workspace( - root, - new NodeJsSyncHost(), - ); - - workspace.loadWorkspaceFromHost(file).subscribe(); - cachedWorkspaces.set(level, workspace); - - return workspace; -} - -export function createGlobalSettings(): string { - const home = os.homedir(); - if (!home) { - throw new Error('No home directory found.'); - } - - const globalPath = path.join(home, globalFileName); - writeFileSync(globalPath, JSON.stringify({ version: 1 })); - - return globalPath; -} - -export function getWorkspaceRaw( - level: 'local' | 'global' = 'local', -): [JsonAstObject | null, string | null] { - let configPath = level === 'local' ? projectFilePath() : globalFilePath(); - - if (!configPath) { - if (level === 'global') { - configPath = createGlobalSettings(); - } else { - return [null, null]; - } - } - - let content = ''; - new NodeJsSyncHost().read(normalize(configPath)) - .subscribe(data => content = virtualFs.fileBufferToString(data)); - - const ast = parseJsonAst(content, JsonParseMode.Loose); - - if (ast.kind != 'object') { - throw new Error('Invalid JSON'); - } - - return [ast, configPath]; -} - -export function validateWorkspace(json: JsonObject) { - const workspace = new experimental.workspace.Workspace( - normalize('.'), - new NodeJsSyncHost(), - ); - - let error; - workspace.loadWorkspaceFromJson(json).subscribe({ - error: e => error = e, - }); - - if (error) { - throw error; - } - - return true; -} - -function getProjectByCwd(workspace: experimental.workspace.Workspace): string | null { - try { - return workspace.getProjectByPath(normalize(process.cwd())); - } catch (e) { - if (e instanceof experimental.workspace.AmbiguousProjectPathException) { - return workspace.getDefaultProjectName(); - } - throw e; - } -} - -export function getPackageManager(): string { - let workspace = getWorkspace('local'); - - if (workspace) { - const project = getProjectByCwd(workspace); - if (project && workspace.getProjectCli(project)) { - const value = workspace.getProjectCli(project)['packageManager']; - if (typeof value == 'string') { - return value; - } - } - if (workspace.getCli()) { - const value = workspace.getCli()['packageManager']; - if (typeof value == 'string') { - return value; - } - } - } - - workspace = getWorkspace('global'); - if (workspace && workspace.getCli()) { - const value = workspace.getCli()['packageManager']; - if (typeof value == 'string') { - return value; - } - } - - // Only check legacy if updated workspace is not found. - if (!workspace) { - const legacyPackageManager = getLegacyPackageManager(); - if (legacyPackageManager !== null) { - return legacyPackageManager; - } - } - - return 'npm'; -} - -export function migrateLegacyGlobalConfig(): boolean { - const homeDir = os.homedir(); - if (homeDir) { - const legacyGlobalConfigPath = path.join(homeDir, '.angular-cli.json'); - if (existsSync(legacyGlobalConfigPath)) { - const content = readFileSync(legacyGlobalConfigPath, 'utf-8'); - const legacy = parseJson(content, JsonParseMode.Loose); - if (!legacy || typeof legacy != 'object' || Array.isArray(legacy)) { - return false; - } - - const cli: JsonObject = {}; - - if (legacy.packageManager && typeof legacy.packageManager == 'string' - && legacy.packageManager !== 'default') { - cli['packageManager'] = legacy.packageManager; - } - - if (legacy.defaults && typeof legacy.defaults == 'object' && !Array.isArray(legacy.defaults) - && legacy.defaults.schematics && typeof legacy.defaults.schematics == 'object' - && !Array.isArray(legacy.defaults.schematics) - && typeof legacy.defaults.schematics.collection == 'string') { - cli['defaultCollection'] = legacy.defaults.schematics.collection; - } - - if (legacy.warnings && typeof legacy.warnings == 'object' - && !Array.isArray(legacy.warnings)) { - - const warnings: JsonObject = {}; - if (typeof legacy.warnings.versionMismatch == 'boolean') { - warnings['versionMismatch'] = legacy.warnings.versionMismatch; - } - if (typeof legacy.warnings.typescriptMismatch == 'boolean') { - warnings['typescriptMismatch'] = legacy.warnings.typescriptMismatch; - } - - if (Object.getOwnPropertyNames(warnings).length > 0) { - cli['warnings'] = warnings; - } - } - - if (Object.getOwnPropertyNames(cli).length > 0) { - const globalPath = path.join(homeDir, globalFileName); - writeFileSync(globalPath, JSON.stringify({ version: 1, cli }, null, 2)); - - return true; - } - } - } - - return false; -} - -// Fallback, check for packageManager in config file in v1.* global config. -function getLegacyPackageManager(): string | null { - const homeDir = os.homedir(); - if (homeDir) { - const legacyGlobalConfigPath = path.join(homeDir, '.angular-cli.json'); - if (existsSync(legacyGlobalConfigPath)) { - const content = readFileSync(legacyGlobalConfigPath, 'utf-8'); - - const legacy = parseJson(content, JsonParseMode.Loose); - if (!legacy || typeof legacy != 'object' || Array.isArray(legacy)) { - return null; - } - - if (legacy.packageManager && typeof legacy.packageManager === 'string' - && legacy.packageManager !== 'default') { - return legacy.packageManager; - } - } - } - - return null; -} - -export function getDefaultSchematicCollection(): string { - let workspace = getWorkspace('local'); - - if (workspace) { - const project = getProjectByCwd(workspace); - if (project && workspace.getProjectCli(project)) { - const value = workspace.getProjectCli(project)['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - if (workspace.getCli()) { - const value = workspace.getCli()['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - } - - workspace = getWorkspace('global'); - if (workspace && workspace.getCli()) { - const value = workspace.getCli()['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - - return '@schematics/angular'; -} - -export function getSchematicDefaults( - collection: string, - schematic: string, - project?: string | null, -): {} { - let result = {}; - const fullName = `${collection}:${schematic}`; - - let workspace = getWorkspace('global'); - if (workspace && workspace.getSchematics()) { - const schematicObject = workspace.getSchematics()[fullName]; - if (schematicObject) { - result = { ...result, ...(schematicObject as {}) }; - } - const collectionObject = workspace.getSchematics()[collection]; - if (typeof collectionObject == 'object' && !Array.isArray(collectionObject)) { - result = { ...result, ...(collectionObject[schematic] as {}) }; - } - - } - - workspace = getWorkspace('local'); - - if (workspace) { - if (workspace.getSchematics()) { - const schematicObject = workspace.getSchematics()[fullName]; - if (schematicObject) { - result = { ...result, ...(schematicObject as {}) }; - } - const collectionObject = workspace.getSchematics()[collection]; - if (typeof collectionObject == 'object' && !Array.isArray(collectionObject)) { - result = { ...result, ...(collectionObject[schematic] as {}) }; - } - } - - project = project || getProjectByCwd(workspace); - if (project && workspace.getProjectSchematics(project)) { - const schematicObject = workspace.getProjectSchematics(project)[fullName]; - if (schematicObject) { - result = { ...result, ...(schematicObject as {}) }; - } - const collectionObject = workspace.getProjectSchematics(project)[collection]; - if (typeof collectionObject == 'object' && !Array.isArray(collectionObject)) { - result = { ...result, ...(collectionObject[schematic] as {}) }; - } - } - } - - return result; -} - -export function isWarningEnabled(warning: string): boolean { - let workspace = getWorkspace('local'); - - if (workspace) { - const project = getProjectByCwd(workspace); - if (project && workspace.getProjectCli(project)) { - const warnings = workspace.getProjectCli(project)['warnings']; - if (typeof warnings == 'object' && !Array.isArray(warnings)) { - const value = warnings[warning]; - if (typeof value == 'boolean') { - return value; - } - } - } - if (workspace.getCli()) { - const warnings = workspace.getCli()['warnings']; - if (typeof warnings == 'object' && !Array.isArray(warnings)) { - const value = warnings[warning]; - if (typeof value == 'boolean') { - return value; - } - } - } - } - - workspace = getWorkspace('global'); - if (workspace && workspace.getCli()) { - const warnings = workspace.getCli()['warnings']; - if (typeof warnings == 'object' && !Array.isArray(warnings)) { - const value = warnings[warning]; - if (typeof value == 'boolean') { - return value; - } - } - } - - return true; -} diff --git a/packages/angular/cli/utilities/find-up.ts b/packages/angular/cli/utilities/find-up.ts deleted file mode 100644 index 81891a96e565..000000000000 --- a/packages/angular/cli/utilities/find-up.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 { existsSync } from 'fs'; -import * as path from 'path'; - -export function findUp(names: string | string[], from: string) { - 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 (existsSync(p)) { - return p; - } - } - - currentDir = path.dirname(currentDir); - } - - return null; -} diff --git a/packages/angular/cli/utilities/project.ts b/packages/angular/cli/utilities/project.ts deleted file mode 100644 index d6becfa363f1..000000000000 --- a/packages/angular/cli/utilities/project.ts +++ /dev/null @@ -1,75 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { normalize } from '@angular-devkit/core'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { findUp } from './find-up'; - -export function insideProject(): boolean { - return getProjectDetails() !== null; -} - -export interface ProjectDetails { - root: string; - configFile?: string; -} - -export function getProjectDetails(): ProjectDetails | null { - const currentDir = process.cwd(); - const possibleConfigFiles = [ - 'angular.json', - '.angular.json', - 'angular-cli.json', - '.angular-cli.json', - ]; - const configFilePath = findUp(possibleConfigFiles, currentDir); - if (configFilePath === null) { - return null; - } - const configFileName = path.basename(configFilePath); - - const possibleDir = path.dirname(configFilePath); - - const homedir = os.homedir(); - if (normalize(possibleDir) === normalize(homedir)) { - const packageJsonPath = path.join(possibleDir, 'package.json'); - if (!fs.existsSync(packageJsonPath)) { - // No package.json - return null; - } - const packageJsonBuffer = fs.readFileSync(packageJsonPath); - const packageJsonText = packageJsonBuffer === null ? '{}' : packageJsonBuffer.toString(); - const packageJson = JSON.parse(packageJsonText); - if (!containsCliDep(packageJson)) { - // No CLI dependency - return null; - } - } - - return { - root: possibleDir, - configFile: configFileName, - }; -} - -function containsCliDep(obj: any): boolean { - const pkgName = '@angular/cli'; - if (obj) { - if (obj.dependencies && obj.dependencies[pkgName]) { - return true; - } - if (obj.devDependencies && obj.devDependencies[pkgName]) { - return true; - } - } - - return false; -} diff --git a/packages/angular/cli/utilities/require-project-module.ts b/packages/angular/cli/utilities/require-project-module.ts deleted file mode 100644 index 09d722738ac2..000000000000 --- a/packages/angular/cli/utilities/require-project-module.ts +++ /dev/null @@ -1,17 +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 - */ - -// resolve dependencies within the target project -export function resolveProjectModule(root: string, moduleName: string) { - return require.resolve(moduleName, { paths: [root] }); -} - -// require dependencies within the target project -export function requireProjectModule(root: string, moduleName: string) { - return require(resolveProjectModule(root, moduleName)); -} diff --git a/packages/angular/cli/utilities/schematics.ts b/packages/angular/cli/utilities/schematics.ts deleted file mode 100644 index 0a1244b618b7..000000000000 --- a/packages/angular/cli/utilities/schematics.ts +++ /dev/null @@ -1,62 +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 - */ - -// tslint:disable:no-global-tslint-disable no-any -import { schema } from '@angular-devkit/core'; -import { - Collection, - Engine, - Schematic, - SchematicEngine, - formats, -} from '@angular-devkit/schematics'; -import { - FileSystemCollectionDesc, - FileSystemSchematicDesc, - NodeModulesEngineHost, - validateOptionsWithSchema, -} from '@angular-devkit/schematics/tools'; - -export class UnknownCollectionError extends Error { - constructor(collectionName: string) { - super(`Invalid collection (${collectionName}).`); - } -} - -const engineHost = new NodeModulesEngineHost(); -const engine: Engine - = new SchematicEngine(engineHost); - -// Add support for schemaJson. -const registry = new schema.CoreSchemaRegistry(formats.standardFormats); -engineHost.registerOptionsTransform(validateOptionsWithSchema(registry)); - - -export function getEngineHost() { - return engineHost; -} -export function getEngine(): Engine { - return engine; -} - -export function getCollection(collectionName: string): Collection { - const engine = getEngine(); - const collection = engine.createCollection(collectionName); - - if (collection === null) { - throw new UnknownCollectionError(collectionName); - } - - return collection; -} - -export function getSchematic(collection: Collection, - schematicName: string, - allowPrivate?: boolean): Schematic { - return collection.createSchematic(schematicName, allowPrivate); -} diff --git a/packages/angular/create/BUILD.bazel b/packages/angular/create/BUILD.bazel new file mode 100644 index 000000000000..37d46ad44ced --- /dev/null +++ b/packages/angular/create/BUILD.bazel @@ -0,0 +1,50 @@ +# 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.dev/license + +load("//tools:defaults.bzl", "npm_package", "ts_project") + +licenses(["notice"]) + +RUNTIME_ASSETS = glob( + include = [ + "src/*.js", + "src/*.mjs", + ], +) + [ + "package.json", +] + +ts_project( + name = "create", + srcs = glob([ + "src/*.ts", + ]), + data = RUNTIME_ASSETS, + deps = [ + "//:node_modules/@types/node", + "//packages/angular/cli:angular-cli", + ], +) + +genrule( + name = "license", + srcs = ["//:LICENSE"], + outs = ["LICENSE"], + cmd = "cp $(execpath //:LICENSE) $@", +) + +npm_package( + name = "pkg", + pkg_deps = [ + "//packages/angular/cli:package.json", + ], + tags = ["release-package"], + visibility = ["//visibility:public"], + deps = RUNTIME_ASSETS + [ + ":README.md", + ":create", + ":license", + ], +) diff --git a/packages/angular/create/README.md b/packages/angular/create/README.md new file mode 100644 index 000000000000..46135476e406 --- /dev/null +++ b/packages/angular/create/README.md @@ -0,0 +1,31 @@ +# `@angular/create` + +## Create an Angular CLI workspace + +Scaffold an Angular CLI workspace without needing to install the Angular CLI globally. All of the [ng new](https://angular.dev/cli/new) options and features are supported. + +## Usage + +### npm + +``` +npm init @angular@latest [project-name] -- [...options] +``` + +### yarn + +``` +yarn create @angular [project-name] [...options] +``` + +### pnpm + +``` +pnpm create @angular [project-name] [...options] +``` + +### bun + +``` +bun create @angular [project-name] [...options] +``` diff --git a/packages/angular/create/package.json b/packages/angular/create/package.json new file mode 100644 index 000000000000..a5ad3fce4ff9 --- /dev/null +++ b/packages/angular/create/package.json @@ -0,0 +1,16 @@ +{ + "name": "@angular/create", + "version": "0.0.0-PLACEHOLDER", + "description": "Scaffold an Angular CLI workspace.", + "keywords": [ + "angular", + "angular-cli", + "Angular CLI", + "code generation", + "schematics" + ], + "bin": "./src/index.js", + "dependencies": { + "@angular/cli": "0.0.0-PLACEHOLDER" + } +} diff --git a/packages/angular/create/src/index.ts b/packages/angular/create/src/index.ts new file mode 100644 index 000000000000..47343ae9014d --- /dev/null +++ b/packages/angular/create/src/index.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node +/** + * @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 { spawnSync } from 'node:child_process'; +import { join } from 'node:path'; + +const binPath = join(require.resolve('@angular/cli/package.json'), '../bin/ng.js'); +const args = process.argv.slice(2); + +const hasPackageManagerArg = args.some((a) => a.startsWith('--package-manager')); +if (!hasPackageManagerArg) { + // Ex: yarn/1.22.18 npm/? node/v16.15.1 linux x64 + const packageManager = process.env['npm_config_user_agent']?.split('/')[0]; + if (packageManager && ['npm', 'pnpm', 'yarn', 'cnpm', 'bun'].includes(packageManager)) { + args.push('--package-manager', packageManager); + } +} + +// Invoke ng new with any parameters provided. +const { error } = spawnSync(process.execPath, [binPath, 'new', ...args], { + stdio: 'inherit', +}); + +if (error) { + // eslint-disable-next-line no-console + console.error(error); + process.exitCode = 1; +} diff --git a/packages/angular/pwa/BUILD b/packages/angular/pwa/BUILD deleted file mode 100644 index 06a976adb0f5..000000000000 --- a/packages/angular/pwa/BUILD +++ /dev/null @@ -1,35 +0,0 @@ -# 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 - -licenses(["notice"]) # MIT - -load("//tools:defaults.bzl", "ts_library") - -package(default_visibility = ["//visibility:public"]) - -ts_library( - name = "pwa", - srcs = glob( - ["**/*.ts"], - # Currently, this library is used only with the rollup plugin. - # To make it simpler for downstream repositories to compile this, we - # neither provide compile-time deps as an `npm_install` rule, nor do we - # expect the downstream repository to install @types/webpack[-*] - # So we exclude files that depend on webpack typings. - exclude = [ - "pwa/files/**/*", - "**/*_spec.ts", - "**/*_spec_large.ts", - ], - ), - deps = [ - "//packages/angular_devkit/core", - "//packages/angular_devkit/schematics", - ], - tsconfig = "//:tsconfig.json", - # Borrow the compile-time deps of the typescript compiler - # Just to avoid an extra npm install action. - node_modules = "@build_bazel_rules_typescript_tsc_wrapped_deps//:node_modules", -) diff --git a/packages/angular/pwa/BUILD.bazel b/packages/angular/pwa/BUILD.bazel new file mode 100644 index 000000000000..4fc36b05adc6 --- /dev/null +++ b/packages/angular/pwa/BUILD.bazel @@ -0,0 +1,84 @@ +# 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.dev/license + +load("@npm//:defs.bzl", "npm_link_all_packages") +load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project") +load("//tools:ts_json_schema.bzl", "ts_json_schema") + +licenses(["notice"]) + +package(default_visibility = ["//visibility:public"]) + +npm_link_all_packages() + +RUNTIME_ASSETS = glob( + include = [ + "pwa/*.js", + "pwa/*.mjs", + "pwa/files/**/*", + ], +) + [ + "package.json", + "collection.json", + "pwa/schema.json", +] + +ts_project( + name = "pwa", + srcs = [ + "pwa/index.ts", + "//packages/angular/pwa:pwa/schema.ts", + ], + data = RUNTIME_ASSETS, + deps = [ + ":node_modules/@angular-devkit/schematics", + ":node_modules/@schematics/angular", + ":node_modules/parse5-html-rewriting-stream", + "//:node_modules/@types/node", + ], +) + +ts_json_schema( + name = "pwa_schema", + src = "pwa/schema.json", +) + +ts_project( + name = "pwa_test_lib", + testonly = True, + srcs = glob(["pwa/**/*_spec.ts"]), + deps = [ + ":node_modules/@angular-devkit/schematics", + ":pwa", + "//:node_modules/@types/jasmine", + "//:node_modules/@types/node", + ], +) + +jasmine_test( + name = "pwa_test", + data = [":pwa_test_lib"], +) + +genrule( + name = "license", + srcs = ["//:LICENSE"], + outs = ["LICENSE"], + cmd = "cp $(execpath //:LICENSE) $@", +) + +npm_package( + name = "pkg", + pkg_deps = [ + "//packages/angular_devkit/schematics:package.json", + "//packages/schematics/angular:package.json", + ], + tags = ["release-package"], + deps = RUNTIME_ASSETS + [ + ":README.md", + ":license", + ":pwa", + ], +) diff --git a/packages/angular/pwa/README.md b/packages/angular/pwa/README.md new file mode 100644 index 000000000000..c7ecbdaa99af --- /dev/null +++ b/packages/angular/pwa/README.md @@ -0,0 +1,23 @@ +# `@angular/pwa` + +This is a [schematic](https://angular.dev/tools/cli/schematics) for adding +[Progressive Web App](https://web.dev/progressive-web-apps/) support to an Angular project. Run the +schematic with the [Angular CLI](https://angular.dev/tools/cli): + +```shell +ng add @angular/pwa --project +``` + +Executing the command mentioned above will perform the following actions: + +1. Adds [`@angular/service-worker`](https://npmjs.com/@angular/service-worker) as a dependency to your project. +1. Enables service worker builds in the Angular CLI. +1. Imports and registers the service worker in the application module. +1. Updates the `index.html` file: + - Includes a link to add the [manifest.webmanifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) file. + - Adds a meta tag for `theme-color`. +1. Installs icon files to support the installed Progressive Web App (PWA). +1. Creates the service worker configuration file called `ngsw-config.json`, specifying caching behaviors and other settings. + +See [Getting started with service workers](https://angular.dev/ecosystem/service-workers/getting-started) +for more information. diff --git a/packages/angular/pwa/collection.json b/packages/angular/pwa/collection.json index f0fc0030d59a..7f30895af77d 100644 --- a/packages/angular/pwa/collection.json +++ b/packages/angular/pwa/collection.json @@ -6,6 +6,11 @@ "schema": "./pwa/schema.json", "private": true, "hidden": true + }, + "pwa": { + "factory": "./pwa", + "description": "Update an application with PWA defaults.", + "schema": "./pwa/schema.json" } } } diff --git a/packages/angular/pwa/package.json b/packages/angular/pwa/package.json index 946e0afd70d4..4f6fb37b8f99 100644 --- a/packages/angular/pwa/package.json +++ b/packages/angular/pwa/package.json @@ -1,20 +1,27 @@ { "name": "@angular/pwa", - "version": "0.0.0", + "version": "0.0.0-PLACEHOLDER", "description": "PWA schematics for Angular", "keywords": [ "blueprints", "code generation", "schematics" ], - "scripts": { - "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" - }, "schematics": "./collection.json", + "ng-add": { + "save": false + }, "dependencies": { - "@angular-devkit/core": "0.0.0", - "@angular-devkit/schematics": "0.0.0", - "@schematics/angular": "0.0.0", - "typescript": "~2.6.2" + "@angular-devkit/schematics": "workspace:0.0.0-PLACEHOLDER", + "@schematics/angular": "workspace:0.0.0-PLACEHOLDER", + "parse5-html-rewriting-stream": "7.1.0" + }, + "peerDependencies": { + "@angular/cli": "workspace:^0.0.0-PLACEHOLDER" + }, + "peerDependenciesMeta": { + "@angular/cli": { + "optional": true + } } -} \ No newline at end of file +} diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-128x128.png b/packages/angular/pwa/pwa/files/assets/icons/icon-128x128.png index 9f9241f0be40..5a9a2ccdb34a 100644 Binary files a/packages/angular/pwa/pwa/files/assets/icons/icon-128x128.png and b/packages/angular/pwa/pwa/files/assets/icons/icon-128x128.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-144x144.png b/packages/angular/pwa/pwa/files/assets/icons/icon-144x144.png index 4a5f8c16389c..11702cd7bd67 100644 Binary files a/packages/angular/pwa/pwa/files/assets/icons/icon-144x144.png and b/packages/angular/pwa/pwa/files/assets/icons/icon-144x144.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-152x152.png b/packages/angular/pwa/pwa/files/assets/icons/icon-152x152.png index 34a1a8d64587..ff4e06b858a9 100644 Binary files a/packages/angular/pwa/pwa/files/assets/icons/icon-152x152.png and b/packages/angular/pwa/pwa/files/assets/icons/icon-152x152.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-192x192.png b/packages/angular/pwa/pwa/files/assets/icons/icon-192x192.png index 9172e5dd29e4..afd36a48c681 100644 Binary files a/packages/angular/pwa/pwa/files/assets/icons/icon-192x192.png and b/packages/angular/pwa/pwa/files/assets/icons/icon-192x192.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-384x384.png b/packages/angular/pwa/pwa/files/assets/icons/icon-384x384.png index e54e8d3eafe5..613ac793e063 100644 Binary files a/packages/angular/pwa/pwa/files/assets/icons/icon-384x384.png and b/packages/angular/pwa/pwa/files/assets/icons/icon-384x384.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-512x512.png b/packages/angular/pwa/pwa/files/assets/icons/icon-512x512.png index 51ee297df1cb..7574990f2001 100644 Binary files a/packages/angular/pwa/pwa/files/assets/icons/icon-512x512.png and b/packages/angular/pwa/pwa/files/assets/icons/icon-512x512.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-72x72.png b/packages/angular/pwa/pwa/files/assets/icons/icon-72x72.png index 2814a3f30caf..033724e15f54 100644 Binary files a/packages/angular/pwa/pwa/files/assets/icons/icon-72x72.png and b/packages/angular/pwa/pwa/files/assets/icons/icon-72x72.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-96x96.png b/packages/angular/pwa/pwa/files/assets/icons/icon-96x96.png index d271025c4f22..3090dc2d8f93 100644 Binary files a/packages/angular/pwa/pwa/files/assets/icons/icon-96x96.png and b/packages/angular/pwa/pwa/files/assets/icons/icon-96x96.png differ diff --git a/packages/angular/pwa/pwa/files/assets/manifest.webmanifest b/packages/angular/pwa/pwa/files/assets/manifest.webmanifest new file mode 100644 index 000000000000..f8c1e3960511 --- /dev/null +++ b/packages/angular/pwa/pwa/files/assets/manifest.webmanifest @@ -0,0 +1,59 @@ +{ + "name": "<%= title %>", + "short_name": "<%= title %>", + "theme_color": "#1976d2", + "background_color": "#fafafa", + "display": "standalone", + "scope": "./", + "start_url": "./", + "icons": [ + { + "src": "<%= iconsPath %>/icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "<%= iconsPath %>/icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "<%= iconsPath %>/icon-128x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "<%= iconsPath %>/icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "<%= iconsPath %>/icon-152x152.png", + "sizes": "152x152", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "<%= iconsPath %>/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "<%= iconsPath %>/icon-384x384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "<%= iconsPath %>/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable any" + } + ] +} diff --git a/packages/angular/pwa/pwa/files/root/manifest.json b/packages/angular/pwa/pwa/files/root/manifest.json deleted file mode 100644 index 37cf5fae506f..000000000000 --- a/packages/angular/pwa/pwa/files/root/manifest.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "<%= title %>", - "short_name": "<%= title %>", - "theme_color": "#1976d2", - "background_color": "#fafafa", - "display": "standalone", - "scope": "/", - "start_url": "/", - "icons": [ - { - "src": "assets/icons/icon-72x72.png", - "sizes": "72x72", - "type": "image/png" - }, - { - "src": "assets/icons/icon-96x96.png", - "sizes": "96x96", - "type": "image/png" - }, - { - "src": "assets/icons/icon-128x128.png", - "sizes": "128x128", - "type": "image/png" - }, - { - "src": "assets/icons/icon-144x144.png", - "sizes": "144x144", - "type": "image/png" - }, - { - "src": "assets/icons/icon-152x152.png", - "sizes": "152x152", - "type": "image/png" - }, - { - "src": "assets/icons/icon-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "assets/icons/icon-384x384.png", - "sizes": "384x384", - "type": "image/png" - }, - { - "src": "assets/icons/icon-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ] -} \ No newline at end of file diff --git a/packages/angular/pwa/pwa/index.ts b/packages/angular/pwa/pwa/index.ts index fc5b40203427..f78095b7a564 100644 --- a/packages/angular/pwa/pwa/index.ts +++ b/packages/angular/pwa/pwa/index.ts @@ -1,14 +1,13 @@ /** -* @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, join, normalize } from '@angular-devkit/core'; + * @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 { Rule, - SchematicContext, SchematicsException, Tree, apply, @@ -19,154 +18,181 @@ import { template, url, } from '@angular-devkit/schematics'; -import { getWorkspace, getWorkspacePath } from '../utility/config'; +import { readWorkspace, writeWorkspace } from '@schematics/angular/utility'; +import { posix } from 'node:path'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; import { Schema as PwaOptions } from './schema'; +function updateIndexFile(path: string): Rule { + return async (host: Tree) => { + const originalContent = host.readText(path); -function addServiceWorker(options: PwaOptions): Rule { - return (host: Tree, context: SchematicContext) => { - context.logger.debug('Adding service worker...'); - - const swOptions = { - ...options, - }; - delete swOptions.title; + const { RewritingStream } = await loadEsmModule( + 'parse5-html-rewriting-stream', + ); - return externalSchematic('@schematics/angular', 'service-worker', swOptions); - }; -} + const rewriter = new RewritingStream(); + let needsNoScript = true; + rewriter.on('startTag', (startTag) => { + if (startTag.tagName === 'noscript') { + needsNoScript = false; + } -function getIndent(text: string): string { - let indent = ''; + rewriter.emitStartTag(startTag); + }); - for (const char of text) { - if (char === ' ' || char === '\t') { - indent += char; - } else { - break; - } - } + rewriter.on('endTag', (endTag) => { + if (endTag.tagName === 'head') { + rewriter.emitRaw(' \n'); + rewriter.emitRaw(' \n'); + } else if (endTag.tagName === 'body' && needsNoScript) { + rewriter.emitRaw( + ' \n', + ); + } - return indent; -} + rewriter.emitEndTag(endTag); + }); -function updateIndexFile(options: PwaOptions): Rule { - return (host: Tree, context: SchematicContext) => { - const workspace = getWorkspace(host); - const project = workspace.projects[options.project as string]; - let path: string; - if (project && project.architect && project.architect.build && - project.architect.build.options.index) { - path = project.architect.build.options.index; - } else { - throw new SchematicsException('Could not find index file for the project'); - } - const buffer = host.read(path); - if (buffer === null) { - throw new SchematicsException(`Could not read index file: ${path}`); - } - const content = buffer.toString(); - const lines = content.split('\n'); - let closingHeadTagLineIndex = -1; - let closingBodyTagLineIndex = -1; - lines.forEach((line, index) => { - if (closingHeadTagLineIndex === -1 && /<\/head>/.test(line)) { - closingHeadTagLineIndex = index; - } else if (closingBodyTagLineIndex === -1 && /<\/body>/.test(line)) { - closingBodyTagLineIndex = index; + return pipeline(Readable.from(originalContent), rewriter, async function (source) { + const chunks = []; + for await (const chunk of source) { + chunks.push(Buffer.from(chunk)); } + host.overwrite(path, Buffer.concat(chunks)); }); - - const headIndent = getIndent(lines[closingHeadTagLineIndex]) + ' '; - const itemsToAddToHead = [ - '', - '', - ]; - - const bodyIndent = getIndent(lines[closingBodyTagLineIndex]) + ' '; - const itemsToAddToBody = [ - '', - ]; - - const updatedIndex = [ - ...lines.slice(0, closingHeadTagLineIndex), - ...itemsToAddToHead.map(line => headIndent + line), - ...lines.slice(closingHeadTagLineIndex, closingBodyTagLineIndex), - ...itemsToAddToBody.map(line => bodyIndent + line), - ...lines.slice(closingHeadTagLineIndex), - ].join('\n'); - - host.overwrite(path, updatedIndex); - - return host; }; } -function addManifestToAssetsConfig(options: PwaOptions) { - return (host: Tree, context: SchematicContext) => { - - const workspacePath = getWorkspacePath(host); - const workspace = getWorkspace(host); - const project = workspace.projects[options.project as string]; - - if (!project) { - throw new Error(`Project is not defined in this workspace.`); +export default function (options: PwaOptions): Rule { + return async (host) => { + if (!options.title) { + options.title = options.project; } - const assetEntry = join(normalize(project.root), 'src', 'manifest.json'); + const workspace = await readWorkspace(host); - if (!project.architect) { - throw new Error(`Architect is not defined for this project.`); + if (!options.project) { + throw new SchematicsException('Option "project" is required.'); } - const architect = project.architect; - - ['build', 'test'].forEach((target) => { + const project = workspace.projects.get(options.project); + if (!project) { + throw new SchematicsException(`Project is not defined in this workspace.`); + } - const applyTo = architect[target].options; - const assets = applyTo.assets || (applyTo.assets = []); + if (project.extensions['projectType'] !== 'application') { + throw new SchematicsException(`PWA requires a project type of "application".`); + } - assets.push(assetEntry); + // Find all the relevant targets for the project + if (project.targets.size === 0) { + throw new SchematicsException(`Targets are not defined for this project.`); + } - }); + const buildTargets = []; + const testTargets = []; + for (const target of project.targets.values()) { + if ( + target.builder === '@angular-devkit/build-angular:browser' || + target.builder === '@angular-devkit/build-angular:application' || + target.builder === '@angular/build:application' + ) { + buildTargets.push(target); + } else if ( + target.builder === '@angular-devkit/build-angular:karma' || + target.builder === '@angular/build:karma' + ) { + testTargets.push(target); + } + } - host.overwrite(workspacePath, JSON.stringify(workspace, null, 2)); + // Find all index.html files in build targets + const indexFiles = new Set(); + let checkForDefaultIndex = false; + for (const target of buildTargets) { + if (typeof target.options?.index === 'string') { + indexFiles.add(target.options.index); + } else if (target.options?.index === undefined) { + checkForDefaultIndex = true; + } - return host; - }; -} + if (!target.configurations) { + continue; + } -export default function (options: PwaOptions): Rule { - return (host: Tree) => { - const workspace = getWorkspace(host); - if (!options.project) { - throw new SchematicsException('Option "project" is required.'); - } - const project = workspace.projects[options.project]; - if (project.projectType !== 'application') { - throw new SchematicsException(`PWA requires a project type of "application".`); + for (const options of Object.values(target.configurations)) { + if (typeof options?.index === 'string') { + indexFiles.add(options.index); + } else if (options?.index === undefined) { + checkForDefaultIndex = true; + } + } } - const sourcePath = join(project.root as Path, 'src'); - const assetsPath = join(sourcePath, 'assets'); + // Setup sources for the assets files to add to the project + const sourcePath = project.sourceRoot ?? posix.join(project.root, 'src'); - options.title = options.title || options.project; + // Check for a default index file if a configuration path allows for a default usage + if (checkForDefaultIndex) { + const defaultIndexFile = posix.join(sourcePath, 'index.html'); + if (host.exists(defaultIndexFile)) { + indexFiles.add(defaultIndexFile); + } + } - const rootTemplateSource = apply(url('http://github.com/files/root'), [ - template({ ...options }), - move(sourcePath), - ]); - const assetsTemplateSource = apply(url('http://github.com/files/assets'), [ - template({ ...options }), - move(assetsPath), - ]); + // Setup service worker schematic options + const { title, ...swOptions } = options; + + await writeWorkspace(host, workspace); + let assetsDir = posix.join(sourcePath, 'assets'); + let iconsPath: string; + if (host.exists(assetsDir)) { + // Add manifest to asset configuration + const assetEntry = posix.join( + project.sourceRoot ?? posix.join(project.root, 'src'), + 'manifest.webmanifest', + ); + for (const target of [...buildTargets, ...testTargets]) { + if (target.options) { + if (Array.isArray(target.options.assets)) { + target.options.assets.push(assetEntry); + } else { + target.options.assets = [assetEntry]; + } + } else { + target.options = { assets: [assetEntry] }; + } + } + iconsPath = 'assets'; + } else { + assetsDir = posix.join(project.root, 'public'); + iconsPath = 'icons'; + } return chain([ - addServiceWorker(options), - mergeWith(rootTemplateSource), - mergeWith(assetsTemplateSource), - updateIndexFile(options), - addManifestToAssetsConfig(options), + externalSchematic('@schematics/angular', 'service-worker', swOptions), + mergeWith( + apply(url('http://github.com/files/assets'), [template({ ...options, iconsPath }), move(assetsDir)]), + ), + ...[...indexFiles].map((path) => updateIndexFile(path)), ]); }; } + +/** + * 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: string | URL): Promise { + return new Function('modulePath', `return import(modulePath);`)(modulePath) as Promise; +} diff --git a/packages/angular/pwa/pwa/index_spec.ts b/packages/angular/pwa/pwa/index_spec.ts index 312b31a695b1..37677894b446 100644 --- a/packages/angular/pwa/pwa/index_spec.ts +++ b/packages/angular/pwa/pwa/index_spec.ts @@ -1,39 +1,35 @@ /** * @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 { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; -import * as path from 'path'; +import * as path from 'node:path'; import { Schema as PwaOptions } from './schema'; - -// tslint:disable:max-line-length describe('PWA Schematic', () => { const schematicRunner = new SchematicTestRunner( '@angular/pwa', - path.join(__dirname, '../collection.json'), + require.resolve(path.join(__dirname, '../collection.json')), ); const defaultOptions: PwaOptions = { project: 'bar', target: 'build', - configuration: 'production', title: 'Fake Title', }; let appTree: UnitTestTree; - // tslint:disable-next-line:no-any - const workspaceOptions: any = { + const workspaceOptions = { name: 'workspace', newProjectRoot: 'projects', version: '6.0.0', }; - // tslint:disable-next-line:no-any - const appOptions: any = { + const appOptions = { name: 'bar', inlineStyle: false, inlineTemplate: false, @@ -42,68 +38,175 @@ describe('PWA Schematic', () => { skipTests: false, }; - beforeEach(() => { - appTree = schematicRunner.runExternalSchematic('@schematics/angular', 'workspace', workspaceOptions); - appTree = schematicRunner.runExternalSchematic('@schematics/angular', 'application', appOptions, appTree); + beforeEach(async () => { + appTree = await schematicRunner.runExternalSchematic( + '@schematics/angular', + 'workspace', + workspaceOptions, + ); + appTree = await schematicRunner.runExternalSchematic( + '@schematics/angular', + 'application', + appOptions, + appTree, + ); }); - it('should run the service worker schematic', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + it('should create icon files', async () => { + const dimensions = [72, 96, 128, 144, 152, 192, 384, 512]; + const iconPath = '/projects/bar/public/icons/icon-'; + const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + + dimensions.forEach((d) => { + const path = `${iconPath}${d}x${d}.png`; + expect(tree.exists(path)).toBeTrue(); + }); + }); + + it('should reference the icons in the manifest correctly', async () => { + const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + const manifestText = tree.readContent('/projects/bar/public/manifest.webmanifest'); + const manifest = JSON.parse(manifestText); + for (const icon of manifest.icons) { + expect(icon.src).toMatch(/^icons\/icon-\d+x\d+.png/); + } + }); + + it('should run the service worker schematic', async () => { + const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree); const configText = tree.readContent('/angular.json'); const config = JSON.parse(configText); const swFlag = config.projects.bar.architect.build.configurations.production.serviceWorker; - expect(swFlag).toEqual(true); - }); - it('should create icon files', () => { - const dimensions = [72, 96, 128, 144, 152, 192, 384, 512]; - const iconPath = '/projects/bar/src/assets/icons/icon-'; - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - dimensions.forEach(d => { - const path = `${iconPath}${d}x${d}.png`; - expect(tree.exists(path)).toEqual(true); - }); + expect(swFlag).toBe('projects/bar/ngsw-config.json'); }); - it('should create a manifest file', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - expect(tree.exists('/projects/bar/src/manifest.json')).toEqual(true); + it('should create a manifest file', async () => { + const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + expect(tree.exists('/projects/bar/public/manifest.webmanifest')).toBeTrue(); }); - it('should set the name & short_name in the manifest file', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - const manifestText = tree.readContent('/projects/bar/src/manifest.json'); + it('should set the name & short_name in the manifest file', async () => { + const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + + const manifestText = tree.readContent('/projects/bar/public/manifest.webmanifest'); const manifest = JSON.parse(manifestText); + expect(manifest.name).toEqual(defaultOptions.title); expect(manifest.short_name).toEqual(defaultOptions.title); }); - it('should set the name & short_name in the manifest file when no title provided', () => { - const options = {...defaultOptions, title: undefined}; - const tree = schematicRunner.runSchematic('ng-add', options, appTree); - const manifestText = tree.readContent('/projects/bar/src/manifest.json'); + it('should set the name & short_name in the manifest file when no title provided', async () => { + const options = { ...defaultOptions, title: undefined }; + const tree = await schematicRunner.runSchematic('ng-add', options, appTree); + + const manifestText = tree.readContent('/projects/bar/public/manifest.webmanifest'); const manifest = JSON.parse(manifestText); + expect(manifest.name).toEqual(defaultOptions.project); expect(manifest.short_name).toEqual(defaultOptions.project); }); - it('should update the index file', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + it('should update the index file', async () => { + const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree); const content = tree.readContent('projects/bar/src/index.html'); - expect(content).toMatch(//); + expect(content).toMatch(//); expect(content).toMatch(//); - expect(content) - .toMatch(/