diff --git a/.bazelignore b/.bazelignore index de4d1f007dd1..52ebdac6af4a 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1,2 +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 index 722747468c4f..4f79c86cf3b4 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,8 +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 -# Performance: avoid stat'ing input files -build --watchfs +# 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 291a72a17bfd..000000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,345 +0,0 @@ -# Configuration file for https://circleci.com/gh/angular/angular-cli - -# Note: YAML anchors allow an object to be re-used, reducing duplication. -# The ampersand declares an alias for an object, then later the `<<: *name` -# syntax dereferences it. -# See http://blog.daemonl.com/2016/02/yaml.html -# To validate changes, use an online parser, eg. -# http://yaml-online-parser.appspot.com/ - -# Variables - -## IMPORTANT -# If you change the cache key prefix, also sync the restore_cache fallback to match. -# Keep the static part of the cache key as prefix to enable correct fallbacks. -# See https://circleci.com/docs/2.0/caching/#restoring-cache for how prefixes work in CircleCI. -var_1: &default_docker_image circleci/node:12.1 -var_2: &browsers_docker_image circleci/node:12.1-browsers -var_3: &browsers_docker_image_node_10 circleci/node:10.12-browsers -var_4: &cache_key angular_devkit-0.11.0-{{ checksum "yarn.lock" }} - -# Settings common to each job -anchor_1: &defaults - working_directory: ~/ng - docker: - - image: *default_docker_image -anchor_1_win: &defaults_win - working_directory: ~/ng - resource_class: windows.medium - shell: powershell.exe -ExecutionPolicy Bypass - machine: - image: windows-server-2019 - -# After checkout, rebase on top of target branch. -anchor_2: &post_checkout - run: - name: Rebase PR on target branch - command: > - if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then - # User is required for rebase. - git config user.name "angular-ci" - git config user.email "angular-ci" - # Rebase PR on top of target branch. - node tools/rebase-pr.js angular/angular-cli ${CIRCLE_PR_NUMBER} - else - echo "This build is not over a PR, nothing to do." - fi -anchor_2_win: &post_checkout_win - run: - name: Rebase PR on target branch - command: > - if (Test-Path env:CIRCLE_PR_NUMBER) { - git config user.name "angular-ci" - git config user.email "angular-ci" - node tools\rebase-pr.js angular/angular-cli $env:CIRCLE_PR_NUMBER } -anchor_3: &restore_cache - restore_cache: - keys: - - *cache_key - # This fallback should be the cache_key without variables. - - angular_devkit-0.11.0- -anchor_4: &attach_options - at: . -anchor_5: &env_win - run: - # Need to install node and yarn before, as the base windows image doesn't have anything. - # TODO: remove when CircleCI provides preconfigured node images/VMs. - name: Setup windows node environment - command: ./.circleci/windows-env.ps1 -anchor_6: &ignore_pull_requests - filters: - branches: - ignore: - - /pull\/.*/ - -# Job definitions -version: 2.1 -jobs: - install: - <<: *defaults - steps: - - checkout - - *post_checkout - - *restore_cache - - run: yarn install --frozen-lockfile - - persist_to_workspace: - root: . - paths: - - ./* - - save_cache: - key: *cache_key - paths: - - ~/.cache/yarn - - lint: - <<: *defaults - steps: - - attach_workspace: *attach_options - - run: yarn lint - - run: 'yarn bazel:format -mode=check || - (echo "BUILD files not formatted. Please run ''yarn bazel:format''" ; exit 1)' - # Run the skylark linter to check our Bazel rules - - run: 'yarn bazel:lint || - (echo -e "\n.bzl files have lint errors. Please run ''yarn bazel:lint-fix''"; exit 1)' - - validate: - <<: *defaults - steps: - - attach_workspace: *attach_options - - run: yarn validate --ci - - test: - <<: *defaults - steps: - - attach_workspace: *attach_options - - run: yarn test -- --full - - test-large: - <<: *defaults - docker: - - image: *browsers_docker_image - resource_class: large - parallelism: 4 - steps: - - attach_workspace: *attach_options - - run: yarn webdriver-update - - run: yarn test-large --full --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} - - test-large-ivy: - <<: *defaults - docker: - - image: *browsers_docker_image - resource_class: large - parallelism: 4 - steps: - - attach_workspace: *attach_options - - run: yarn webdriver-update - - run: yarn test-large --ivy --full --glob="packages/angular_devkit/build_angular/test/browser/*_spec_large.ts" --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} - - e2e-cli: - <<: *defaults - docker: - - image: *browsers_docker_image - environment: - BASH_ENV: ~/.profile - NPM_CONFIG_PREFIX: ~/.npm-global - resource_class: xlarge - parallelism: 4 - steps: - - attach_workspace: *attach_options - - run: PATH=~/.npm-global/bin:$PATH xvfb-run -a node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} - - store_artifacts: - path: /tmp/dist - destination: cli/new-production - - e2e-cli-node-10: - <<: *defaults - docker: - - image: *browsers_docker_image_node_10 - environment: - BASH_ENV: ~/.profile - NPM_CONFIG_PREFIX: ~/.npm-global - resource_class: xlarge - parallelism: 4 - steps: - - attach_workspace: *attach_options - # Ensure latest npm version to support local package repository - - run: PATH=~/.npm-global/bin:$PATH npm install --global npm - - run: PATH=~/.npm-global/bin:$PATH xvfb-run -a node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} - - e2e-cli-ivy: - <<: *defaults - docker: - - image: *browsers_docker_image - environment: - BASH_ENV: ~/.profile - NPM_CONFIG_PREFIX: ~/.npm-global - resource_class: xlarge - parallelism: 4 - steps: - - attach_workspace: *attach_options - - run: PATH=~/.npm-global/bin:$PATH xvfb-run -a node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} --ivy - - e2e-cli-ng-snapshots: - <<: *defaults - docker: - - image: *browsers_docker_image - environment: - BASH_ENV: ~/.profile - NPM_CONFIG_PREFIX: ~/.npm-global - resource_class: xlarge - parallelism: 4 - steps: - - attach_workspace: *attach_options - - run: PATH=~/.npm-global/bin:$PATH xvfb-run -a node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} --ng-snapshots - - build: - <<: *defaults - steps: - - attach_workspace: *attach_options - - run: yarn admin -- build - - build-bazel: - <<: *defaults - resource_class: xlarge - steps: - - attach_workspace: *attach_options - - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - - run: yarn bazel:test - - snapshot_publish: - <<: *defaults - steps: - - attach_workspace: *attach_options - - run: - name: Decrypt Credentials - # Note: when changing the image, you might have to re-encrypt the credentials with a - # matching version of openssl. - # See https://stackoverflow.com/a/43847627/2116927 for more info. - command: | - openssl aes-256-cbc -d -in .circleci/github_token -k "${KEY}" -out ~/github_token -md md5 - - run: - name: Deployment to Snapshot - command: | - yarn admin -- snapshots --verbose --githubTokenFile=${HOME}/github_token - - publish: - <<: *defaults - steps: - - attach_workspace: *attach_options - - run: - name: Decrypt Credentials - command: | - openssl aes-256-cbc -d -in .circleci/npm_token -k "${KEY}" -out ~/.npmrc - - run: - name: Deployment to NPM - command: | - yarn admin -- publish --verbose - - # Windows jobs - # CircleCI support for Windows jobs is still in preview. - # Docs: https://github.com/CircleCI-Public/windows-preview-docs - test-win: - <<: *defaults_win - # Skipping cache and workspace for now because it takes 10x longer than on linux. - # TODO: when/if CircleCI makes them faster, use cache and workspaces fully. - # Notes: - # - windows needs its own cache key because binaries in node_modules are different. - # - windows might need its own workspace for the same reason. - # - get cache dir on windows via `yarn cache dir` (was `C:\Users\circleci\AppData\Local\Yarn\Cache\v4` last time) - steps: - - checkout - - *env_win - - *post_checkout_win - - run: node --version - - run: yarn --version - - run: yarn install --frozen-lockfile - # Build and test should be on their own jobs, but restoring workspaces is too slow - # so we do it here. - - run: yarn admin -- build - - run: yarn test -- --full - # Run partial e2e suite on PRs only. Master will run the full e2e suite with sharding. - - run: if (Test-Path env:CIRCLE_PR_NUMBER) { node tests\legacy-cli\run_e2e.js "--glob=tests/{basic,ivy}/**" } - - e2e-cli-win: - <<: *defaults_win - parallelism: 4 - steps: - - checkout - - *env_win - # TODO: remove commands other than the e2e runner when workspaces on windows are well supported. - - *post_checkout_win - - run: node --version - - run: yarn --version - - run: yarn install --frozen-lockfile - - run: yarn admin -- build - - run: node tests\legacy-cli\run_e2e.js --nb-shards=$env:CIRCLE_NODE_TOTAL --shard=$env:CIRCLE_NODE_INDEX - -workflows: - version: 2 - default_workflow: - jobs: - - install - - lint: - requires: - - install - - validate: - requires: - - install - - build: - requires: - - install - filters: - branches: - ignore: - - /docs-preview/ - - build-bazel: - requires: - - build - - test: - requires: - - build - - test-win: - requires: - - test - - test-large: - requires: - - build - - test-large-ivy: - requires: - - build - - e2e-cli: - requires: - - build - - e2e-cli-ivy: - requires: - - build - - e2e-cli-ng-snapshots: - <<: *ignore_pull_requests - requires: - - e2e-cli - - e2e-cli-node-10: - <<: *ignore_pull_requests - requires: - - e2e-cli - - e2e-cli-win: - <<: *ignore_pull_requests - requires: - - e2e-cli - - snapshot_publish: - <<: *ignore_pull_requests - requires: - - test - - build - - e2e-cli - - publish: - requires: - - test - - build - - e2e-cli - - 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/.circleci/windows-env.ps1 b/.circleci/windows-env.ps1 deleted file mode 100644 index 1c4fff24e433..000000000000 --- a/.circleci/windows-env.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -# Install nodejs and yarn via Chocolatey. -choco install nodejs --version 12.1.0 --no-progress -choco install yarn --version 1.16.0 --no-progress - -# Add PATH modifications to the Powershell profile. This is the win equivalent of .bash_profile. -# https://docs.microsoft.com/en-us/previous-versions//bb613488(v=vs.85) -new-item -path $profile -itemtype file -force -# Paths for nodejs, npm, and yarn. Use single quotes to prevent interpolation. -Add-Content $profile '$Env:path += ";C:\Program Files\nodejs\;C:\Users\circleci\AppData\Roaming\npm\;C:\Program Files (x86)\Yarn\bin\;"' diff --git a/.editorconfig b/.editorconfig index 1c658dfbc777..c0a70d2acb14 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index b5135def5125..000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,10 +0,0 @@ -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 - -Please help us process issues more efficiently by filing an -issue using one of the following templates: - -https://github.com/angular/angular-cli/issues/new/choose - -Thank you! - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.md b/.github/ISSUE_TEMPLATE/1-bug-report.md deleted file mode 100644 index b8249e87c28b..000000000000 --- a/.github/ISSUE_TEMPLATE/1-bug-report.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -name: "\U0001F41EBug report" -about: Report a bug in Angular CLI ---- - - - -# 🐞 Bug report - -### Command (mark with an `x`) - - -``` -- [ ] new -- [ ] build -- [ ] serve -- [ ] test -- [ ] e2e -- [ ] generate -- [ ] add -- [ ] update -- [ ] lint -- [ ] xi18n -- [ ] run -- [ ] config -- [ ] help -- [ ] version -- [ ] doc -``` - -### Is this a regression? - - - Yes, the previous version in which this bug was not present was: .... - - -### Description - - A clear and concise description of the problem... - - -## 🔬 Minimal Reproduction - - -## 🔥 Exception or Error -

-
-
-
-
- - -## 🌍 Your Environment -

-
-
-
-
- -**Anything else relevant?** - - - 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.md b/.github/ISSUE_TEMPLATE/2-feature-request.md deleted file mode 100644 index 4c8292f45157..000000000000 --- a/.github/ISSUE_TEMPLATE/2-feature-request.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -name: "\U0001F680Feature request" -about: Suggest a feature for Angular CLI - ---- - - - -# 🚀 Feature request - - -### Command (mark with an `x`) - - -``` -- [ ] new -- [ ] build -- [ ] serve -- [ ] test -- [ ] e2e -- [ ] generate -- [ ] add -- [ ] update -- [ ] lint -- [ ] xi18n -- [ ] run -- [ ] config -- [ ] help -- [ ] version -- [ ] doc -``` - -### Description - A clear and concise description of the problem or missing capability... - - -### Describe the solution you'd like - If you have a solution in mind, please describe it. - - -### Describe alternatives you've considered - Have you considered any alternative solutions or workarounds? 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/3-docs-bug.md b/.github/ISSUE_TEMPLATE/3-docs-bug.md deleted file mode 100644 index 7cd9ec28753a..000000000000 --- a/.github/ISSUE_TEMPLATE/3-docs-bug.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: "📚 Docs or angular.io issue report" -about: Report an issue in Angular's documentation or angular.io application - ---- - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 - -Please file any Docs or angular.io issues at: https://github.com/angular/angular/issues/new/choose - -For the time being, we keep Angular AIO issues in a separate repository. - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 diff --git a/.github/ISSUE_TEMPLATE/4-security-issue-disclosure.md b/.github/ISSUE_TEMPLATE/4-security-issue-disclosure.md deleted file mode 100644 index b789da9f6da1..000000000000 --- a/.github/ISSUE_TEMPLATE/4-security-issue-disclosure.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: ⚠️Security issue disclosure -about: Report a security issue in Angular Framework, Material, or CLI - ---- - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 - -Please read https://angular.io/guide/security#report-issues on how to disclose security related issues. - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 diff --git a/.github/ISSUE_TEMPLATE/5-support-request.md b/.github/ISSUE_TEMPLATE/5-support-request.md deleted file mode 100644 index f6e6e66ff893..000000000000 --- a/.github/ISSUE_TEMPLATE/5-support-request.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: "❓Support request" -about: Questions and requests for support - ---- - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 - -Please do not file questions or support requests on the GitHub issues tracker. - -You can get your questions answered using other communication channels. Please see: -https://github.com/angular/angular-cli/blob/master/CONTRIBUTING.md#question - -Thank you! - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 diff --git a/.github/ISSUE_TEMPLATE/6-angular-framework.md b/.github/ISSUE_TEMPLATE/6-angular-framework.md deleted file mode 100644 index 8a689c55de35..000000000000 --- a/.github/ISSUE_TEMPLATE/6-angular-framework.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: "⚡Angular Framework" -about: Issues and feature requests for Angular Framework - ---- - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 - -Please file any Angular Framework issues at: https://github.com/angular/angular/issues/new/choose - -For the time being, we keep Angular issues in a separate repository. - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 diff --git a/.github/ISSUE_TEMPLATE/7-angular-material.md b/.github/ISSUE_TEMPLATE/7-angular-material.md deleted file mode 100644 index b023135b0cc5..000000000000 --- a/.github/ISSUE_TEMPLATE/7-angular-material.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: "\U0001F48EAngular Material" -about: Issues and feature requests for Angular Material - ---- - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 - -Please file any Angular Material issues at: https://github.com/angular/material2/issues/new - -For the time being, we keep Angular Material issues in a separate repository. - -🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 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 index 29e19832903c..1237bc279e11 100644 --- a/.github/SAVED_REPLIES.md +++ b/.github/SAVED_REPLIES.md @@ -4,87 +4,87 @@ The following are canned responses that the Angular CLI team should use to close 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/master/CONTRIBUTING.md#-submitting-an-issue) to understand why we can't act on issues that are lacking important information. +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/master/CONTRIBUTING.md#-submitting-an-issue). +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](http://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/master/CONTRIBUTING.md#-got-a-question-or-problem). ``` +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 to debug these errors: +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 @@ -93,6 +93,7 @@ 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. diff --git a/.github/angular-robot.yml b/.github/angular-robot.yml index c00e502cba2b..3e3a56a25603 100644 --- a/.github/angular-robot.yml +++ b/.github/angular-robot.yml @@ -5,20 +5,22 @@ merge: # the status will be added to your pull requests status: # set to true to disable - disabled: false + disabled: true # the name of the status - context: "ci/angular: merge status" + context: 'ci/angular: merge status' # text to show when all checks pass - successText: "All checks passed!" + successText: 'All checks passed!' # text to show when some checks are failing - failureText: "The following 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. -\nPlease help to unblock it by resolving these conflicts. Thanks!" + 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: "PR action: merge" + mergeLabel: 'action: merge' # list of checks that will determine if the merge label can be added checks: @@ -26,74 +28,65 @@ merge: 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. "PR target:" will work for the label "PR target: master") + # list of labels that a PR needs to have, checked with a regexp (e.g. "target:" will work for the label "target: major") requiredLabels: - - "PR target: *" - - "cla: yes" + - 'target: *' # list of labels that a PR shouldn't have, checked after the required labels with a regexp forbiddenLabels: - - "PR target: TBD" - - "PR action: cleanup" - - "PR action: review" - - "PR state: blocked" - - "cla: no" + - 'action: cleanup' + - 'action: review' + - 'PR state: blocked' # list of PR statuses that need to be successful - requiredStatuses: - - "ci/circleci: build" - - "ci/circleci: build-bazel" - - "ci/circleci: install" - - "ci/circleci: lint" - - "ci/circleci: validate" - - "ci/circleci: test" - - "ci/circleci: test-win" - - "ci/circleci: test-large" + # 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: -\n{{PLACEHOLDER}} -\n -\n**If you want your PR to be merged, it has to pass all the CI checks.** -\n -\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master 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." + 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: false + 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: - - - - "comp: *" + - - 'area: *' # arrays of labels that determine if an issue has been fully triaged l2TriageLabels: - - - - "type: bug/fix" - - "severity*" - - "freq*" - - "comp: *" - - - - "type: feature" - - "comp: *" - - - - "type: refactor" - - "comp: *" - - - - "type: RFC / Discussion / question" - - "comp: *" - - - - "type: docs" - - "comp: *" + - - 'type: bug/fix' + - 'severity*' + - 'freq*' + - 'area: *' + - - 'type: feature' + - 'area: *' + - - 'type: refactor' + - 'area: *' + - - 'type: RFC / Discussion / question' + - 'area: *' + - - 'type: docs' + - 'area: *' # Size checking size: - circleCiStatusName: "ci/circleci: e2e-cli" + # 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 88895c7481f5..de3ad9a9154d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,39 @@ 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/ @@ -19,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 d10bc39a19a7..f651b93dd40b 100644 --- a/.mailmap +++ b/.mailmap @@ -16,8 +16,10 @@ Charles Lyding Charles Lyding <19598772+clydin@users.noreply.github.com> Filipe Silva Mike Brocchi -Alan Agius - Alan Agius +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 81367b00b771..4d5face644df 100644 --- a/.monorepo.json +++ b/.monorepo.json @@ -1,51 +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/cli.svg", - "url": "/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": "/CONTRIBUTING.md", - "Angular CLI": "http://github.com/angular/angular-cli" - }, "packages": { - "@_/benchmark": { - }, - "@_/builders": { - }, - "devkit": { + "@_/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", @@ -58,11 +24,31 @@ ], "snapshotRepo": "angular/cli-builds" }, + "@angular/create": { + "name": "Angular Create", + "section": "Misc", + "links": [ + { + "label": "README", + "url": "/packages/angular/create/README.md" + } + ] + }, "@angular/pwa": { "name": "Angular PWA Schematics", "section": "Schematics", "snapshotRepo": "angular/angular-pwa-builds" }, + "@angular/ssr": { + "name": "Angular SSR", + "links": [ + { + "label": "README", + "url": "/packages/angular/ssr/README.md" + } + ], + "snapshotRepo": "angular/angular-ssr-builds" + }, "@angular-devkit/architect": { "name": "Architect", "links": [ @@ -78,30 +64,6 @@ "section": "Tooling", "snapshotRepo": "angular/angular-devkit-architect-cli-builds" }, - "@angular-devkit/benchmark": { - "name": "Benchmark", - "section": "Tooling" - }, - "@angular-devkit/build-optimizer": { - "name": "Build Optimizer", - "links": [ - { - "label": "README", - "url": "/packages/angular_devkit/build_optimizer/README.md" - } - ], - "snapshotRepo": "angular/angular-devkit-build-optimizer-builds" - }, - "@angular-devkit/build-ng-packagr": { - "name": "Build NgPackagr", - "links": [ - { - "label": "README", - "url": "/packages/angular_devkit/build_ng_packagr/README.md" - } - ], - "snapshotRepo": "angular/angular-devkit-build-ng-packagr-builds" - }, "@angular-devkit/build-angular": { "name": "Build Angular", "links": [ @@ -156,16 +118,6 @@ "name": "Angular Schematics", "section": "Schematics", "snapshotRepo": "angular/schematics-angular-builds" - }, - "@schematics/schematics": { - "name": "Schematics Schematics", - "section": "Schematics", - "snapshotRepo": "angular/schematics-schematics-builds" - }, - "@schematics/update": { - "name": "Package Update Schematics", - "section": "Schematics", - "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 f599e28b8ab0..b8ffd70759fb 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10 +22.15.0 diff --git a/.prettierignore b/.prettierignore index fbcd212375a5..b0b71acaf241 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,18 @@ -/etc/api +/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 index 5e2863a11f68..22e9e4076522 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ { "printWidth": 100, - "singleQuote": true, - "trailingComma": "all" + "quoteProps": "preserve", + "singleQuote": true } diff --git a/BUILD b/BUILD deleted file mode 100644 index a1c5b6b5ac0e..000000000000 --- a/BUILD +++ /dev/null @@ -1,14 +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 - "tsconfig-test.json", # @external - "tslint.base.json", # @external -]) 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 'https://github.com/~font-awesome/scss/font-awesome'; + ``` + + After + + ```scss + @import 'https://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 6072228db9a3..0642e0b7ff65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,8 +26,8 @@ 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 @@ -60,7 +60,7 @@ 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 CLI used -- `.angular-cli.json` or `angular.json` configuration +- `angular.json` configuration - version of Angular DevKit used - 3rd-party libraries and their versions - and most importantly - a use-case that fails @@ -84,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**. @@ -106,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 @@ -130,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: @@ -142,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 @@ -185,16 +192,15 @@ 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. [2] -* **ci**: Changes to our CI configuration files and scripts. [2] -* **docs**: Documentation only changes. -* **feat**: A new feature. [1] -* **fix**: A bug fix. [1] -* **refactor**: A code change that neither fixes a bug nor adds a feature -* **release**: A release commit. Must only include version changes. [2] -* **revert**: A git commit revert. The description must include the original commit message. [2] -* **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.
@@ -205,21 +211,20 @@ The scope should be the name of the npm package affected as perceived by the per 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-ng-packagr** -* **@angular-devkit/build-optimizer** * **@angular-devkit/build-webpack** * **@angular-devkit/core** * **@angular-devkit/schematics** * **@angular-devkit/schematics-cli** * **@ngtools/webpack** * **@schematics/angular** -* **@schematics/schematics** -* **@schematics/update** ### Subject @@ -249,12 +254,32 @@ Just as in the **subject**, use the imperative, present tense: "change" not "cha 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 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: + + + + +Fixes # +``` + +or + +``` +DEPRECATED: + + + + +Closes # +``` -A detailed explanation can be found in this [document][commit-message-format]. +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. + +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 @@ -266,47 +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/blob/master/packages/angular/cli/README.md#development-hints-for-working-on-angular-cli +[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 is protected with TS API Guardian. This is a tool that keeps track of public API surface of our packages. - -To test if your change effect the public API you need to run the API guardian on that particular package. - -For example in case `@angular-devkit/core` package was modified you need to run: - -```bash -yarn bazel test //etc/api:angular_devkit_core_api -``` +Our Public API surface is tracked using golden files. -You can also test all packages by running: +You check all golden files by running: ```bash -yarn bazel test //etc/api ... +pnpm public-api:check ``` If you modified the public API, the test will fail. To update the golden files you need to run: - ```bash -yarn bazel run //etc/api:angular_devkit_core_api.accept -``` - -**Note**: In some cases we use aliased symbols to create namespaces. - -Example: -```javascript -import * as foo from './foo'; - -export { foo }; +pnpm public-api:update ``` -There are currently not supported by the API guardian. -To overcome this limitation we created `_golden-api.ts` in certain packages. - -When adding a new API, it might be the case that you need to add it to `_golden-api.ts`. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8fef94100787..000000000000 --- a/Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM node:10.12 -ENTRYPOINT [ "sh" ] 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 2b50b1394f92..21a7d3f13398 100644 --- a/README.md +++ b/README.md @@ -10,101 +10,188 @@ Any changes to README.md directly will result in a failure on CI. --> -# Angular CLI -### 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](/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/cli.svg)](/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] -### Quick Links -[Gitter](https://gitter.im/angular/angular-cli) | [Contributing](/CONTRIBUTING.md) | [Angular CLI](http://github.com/angular/angular-cli) | -|---|---|---| +## Development Setup ----- -## The Goal of Angular CLI +### Prerequisites -The Angular CLI creates, manages, builds and test your Angular projects. It's built on top of the -Angular DevKit. +- Install [Node.js] which includes [Node Package Manager][npm] -## The Goal of DevKit +### Setting Up a Project -DevKit's 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: -# Getting Started - Local Development +``` +npm install -g @angular/cli +``` -## Installation -To get started locally, follow these instructions: +Create workspace: -1. If you haven't done it already, [make a fork of this repo](https://github.com/angular/angular-cli/fork). -1. Clone to your local computer using `git`. -1. Make sure that you have Node 10.9 or later installed. See instructions [here](https://nodejs.org/en/download/). -1. Make sure that you have `yarn` installed; see instructions [here](https://yarnpkg.com/lang/en/docs/install/). -1. Run `yarn` (no arguments) from the root of your clone of this project. -1. Run `yarn link` to add all custom scripts we use to your global install. +``` +ng new [PROJECT NAME] +``` -## Creating New Packages -Adding a package to this repository means running two separate commands: +Run the application: -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. +``` +cd [PROJECT NAME] +ng serve +``` -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. +Angular is cross-platform, fast, scalable, has incredible tooling, and is loved by millions. -# Packages +## 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 tools and packages: -## Tools +### Tools | Project | Package | Version | Links | |---|---|---|---| +**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) **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 +### Packages | Project | Package | Version | Links | |---|---|---|---| +**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 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)](/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)](/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)](/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) -#### Schematics +#### Misc | 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) -**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 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) -#### Misc +#### Schematics | Project | Package | Version | Links | |---|---|---|---| -**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) +**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 3feedcb097ee..59c97586f6bd 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1,82 +1,289 @@ workspace(name = "angular_cli") -load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +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", - sha256 = "fb87ed5965cef93188af9a7287511639403f4b0da418961ce6defb9dcf658f51", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.27.7/rules_nodejs-0.27.7.tar.gz"], + sha256 = "5dd1e5dea1322174c57d3ca7b899da381d516220793d0adef3ba03b9d23baa8e", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.3/rules_nodejs-5.8.3.tar.gz"], ) -# We use protocol buffers for the Build Event Protocol -git_repository( - name = "com_google_protobuf", - commit = "beaeaeda34e97a6ff9735b33a66e011102ab506b", - remote = "https://github.com/protocolbuffers/protobuf", +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("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") +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") -protobuf_deps() +rules_pkg_dependencies() -load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install") +# Setup the Node.js toolchain +load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains") -# 0.18.0 is needed for .bazelignore -check_bazel_version(minimum_bazel_version = "0.18.0") +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"), +} -node_repositories( +# 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 = { - "10.9.0-darwin_amd64": ( - "node-v10.9.0-darwin-x64.tar.gz", - "node-v10.9.0-darwin-x64", - "3c4fe75dacfcc495a432a7ba2dec9045cff359af2a5d7d0429c84a424ef686fc", - ), - "10.9.0-linux_amd64": ( - "node-v10.9.0-linux-x64.tar.xz", - "node-v10.9.0-linux-x64", - "c5acb8b7055ee0b6ac653dc4e458c5db45348cecc564b388f4ed1def84a329ff", - ), - "10.9.0-windows_amd64": ( - "node-v10.9.0-win-x64.zip", - "node-v10.9.0-win-x64", - "6a75cdbb69d62ed242d6cbf0238a470bcbf628567ee339d4d098a5efcda2401e", - ), - }, - node_version = "10.9.0", - yarn_repositories = { - "1.9.2": ( - "yarn-v1.9.2.tar.gz", - "yarn-v1.9.2", - "3ad69cc7f68159a562c676e21998eb21b44138cae7e8fe0749a7d620cf940204", - ), + "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"), }, - yarn_version = "1.9.2", + 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", ) -yarn_install( +load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "aspect_bazel_lib_register_toolchains") + +aspect_bazel_lib_dependencies() + +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 = [ - "//:tools/yarn/check-yarn.js", + "//: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", ], - package_json = "//:package.json", - yarn_lock = "//:yarn.lock", + 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", +) + +load("@npm//:repositories.bzl", "npm_repositories") + +npm_repositories() + +http_archive( + 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("@aspect_rules_ts//ts:repositories.bzl", "rules_ts_dependencies") + +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"], ) -load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies") +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", +) -install_bazel_dependencies() +load("@aspect_rules_jasmine//jasmine:dependencies.bzl", "rules_jasmine_dependencies") -load("@npm_bazel_typescript//:defs.bzl", "ts_setup_workspace") +rules_jasmine_dependencies() -ts_setup_workspace() +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") -# Load karma dependencies -load("@npm_bazel_karma//:package.bzl", "rules_karma_dependencies") +git_repository( + name = "devinfra", + commit = "a4538b2474d3c551f0217c3d1f5f3a99cf4f8eb7", + remote = "https://github.com/angular/dev-infra.git", +) -rules_karma_dependencies() +load("@devinfra//bazel:setup_dependencies_1.bzl", "setup_dependencies_1") -# Setup the rules_webtesting toolchain -load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories") +setup_dependencies_1() -web_test_repositories() +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", +) + +http_archive( + 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("@aspect_rules_esbuild//esbuild:dependencies.bzl", "rules_esbuild_dependencies") + +rules_esbuild_dependencies() + +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/benchmark/aio/.gitignore b/benchmark/aio/.gitignore deleted file mode 100644 index 3c85010e11ea..000000000000 --- a/benchmark/aio/.gitignore +++ /dev/null @@ -1 +0,0 @@ -angular/ \ No newline at end of file diff --git a/benchmark/aio/package.json b/benchmark/aio/package.json deleted file mode 100644 index fce9b14e36f7..000000000000 --- a/benchmark/aio/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "aio-benchmark", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "initialize": "yarn clone && yarn setup && yarn update", - "clone": "(git clone https://github.com/angular/angular --depth 1 || true) && cd angular && git fetch origin dd2a650c3455f3bc0a88f8181758a84aacb25fea && git checkout -f FETCH_HEAD", - "setup": "cd angular && yarn && cd aio && yarn && yarn setup", - "update": "cd angular/aio && yarn add ../../../../dist/_angular-devkit_build-angular.tgz --dev", - "//": "Shouldn't need to install the package twice, but the first install seems to leave two @ngtools/webpack installs around.", - "postupdate": "cd angular/aio && yarn add ../../../../dist/_angular-devkit_build-angular.tgz --dev", - "benchmark": "cd angular/aio && benchmark --verbose -- yarn ~~build --configuration=stable" - }, - "keywords": [], - "author": "", - "license": "ISC" -} diff --git a/bin/README.md b/bin/README.md deleted file mode 100644 index 6f3d6c17fbd2..000000000000 --- a/bin/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# `/bin` Folder - -This folder includes binaries that are linked when globally linking this repository. - -Each file in this directory follows this pattern: - -1. JavaScript only. -1. Requires `../lib/bootstrap-local.js` to bootstrap TypeScript and Node integration. -1. Requires `../lib/packages` and use the package metadata to find the binary script for the -package the script is bootstrapping. -1. Call out main, or simply require the file if it has no export. - -`devkit-admin` does not follow this pattern as it needs to setup logging and run some localized -logic. - -In order to add a new script, you should make sure it's in the root `package.json`, so people -linking this repo get a reference to the script. 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/benchmark b/bin/benchmark deleted file mode 100755 index 4ada663d8bfc..000000000000 --- a/bin/benchmark +++ /dev/null @@ -1,19 +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; -const main = require(packages['@angular-devkit/benchmark'].bin['benchmark']).main; - -const args = process.argv.slice(2); -main({ args }) - .then(exitCode => process.exitCode = exitCode) - .catch(e => { throw (e); }); 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 f356b0d42e96..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); - -const cwd = process.cwd(); -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, cwd)) - .then(exitCode => process.exit(exitCode || 0)) - .catch(err => { - logger.fatal(err && 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/schematics b/bin/schematics deleted file mode 100755 index ea3f5176befa..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']).main({ args: process.argv.slice(2) }); 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 index 6fdee803e80c..1aa9fc8f9262 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,20 +1,12 @@ # `/docs` Folder -This folder is used for all documentation. It contains a number of subfolders with short +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/documentation` - -THIS FOLDER IS DEPRECATED AND SHOULD NOT BE UPDATED ANYMORE. Documentation is now available on -angular.io and can be updated on the main [angular/angular](https://github.com/angular/angular) -repo. - -Markdown files used to publish to the [GitHub Wiki](https://github.com/angular/angular-cli/wiki). - ## `/docs/process` Description of various caretaker and workflow processes. diff --git a/docs/design/analytics.md b/docs/design/analytics.md index 5ec9c56adf29..0eb3bad26d8f 100644 --- a/docs/design/analytics.md +++ b/docs/design/analytics.md @@ -1,20 +1,11 @@ # 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. -# Pageview -Each command creates a pageview with the path `/command/${commandName}/${subcommandName}`. IE. -`ng generate component my-component --dryRun` would create a page view with the path -`/command/generate/@schematics_angular/component`. - -We use page views to keep track of sessions more effectively, and to tag events to a page. +## Dimensions and Metrics -Project names and target names will be removed. -The command `ng run some-project:lint:some-configuration` will create a page view with the path -`/command/run`. - -# Dimensions Google Analytics Custom Dimensions are used to track system values and flag values. These dimensions are aggregated automatically on the backend. @@ -22,103 +13,89 @@ One dimension per flag, and although technically there can be an overlap between 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. -To create a new dimension (tracking a new flag): - -1. Create the dimension on analytics.google.com first. Dimensions are not tracked if they aren't - defined on GA. +### 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. Add a new row to the table below in the same PR as the one adding the dimension to the code. -1. New dimensions PRs need to be approved by [bradgreen@google.com](mailto:bradgreen@google.com), - [stephenfluin@google.com](mailto:stephenfluin@google.com) and - [iminar@google.com](mailto:iminar@google.com). **This is not negotiable.** +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.** -Note: There's a limit of 20 custom dimensions. +### 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 -### List Of All Dimensions -| Id | Flag | Type | +| Name | Parameter | Type | |:---:|:---|:---| -| 1 | `CPU Count` | `number` | -| 2 | `CPU Speed` | `number` | -| 3 | `RAM (In GB)` | `number` | -| 4 | `Node Version` | `number` | -| 5 | `Flag: --style` | `string` | -| 6 | `--collection` | `string` | -| 7 | `--buildEventLog` | `boolean` | -| 8 | `Flag: --enableIvy` | `boolean` | -| 9 | `Flag: --inlineStyle` | `boolean` | -| 10 | `Flag: --inlineTemplate` | `boolean` | -| 11 | `Flag: --viewEncapsulation` | `string` | -| 12 | `Flag: --skipTests` | `boolean` | -| 13 | `Flag: --aot` | `boolean` | -| 14 | `Flag: --minimal` | `boolean` | -| 15 | `Flag: --lintFix` | `boolean` | -| 16 | `Flag: --optimization` | `boolean` | -| 17 | `Flag: --routing` | `boolean` | -| 18 | `Flag: --skipImport` | `boolean` | -| 19 | `Flag: --export` | `boolean` | -| 20 | `Build Errors (comma separated)` | `string` | +| 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` | -# Metrics +### List of Event Custom Metrics -### List of All Metrics -| Id | Flag | Type | +| Name | Parameter | Type | |:---:|:---|:---| -| 1 | `NgComponentCount` | `number` | -| 2 | `UNUSED_2` | `none` | -| 3 | `UNUSED_3` | `none` | -| 4 | `UNUSED_4` | `none` | -| 5 | `Build Time` | `number` | -| 6 | `NgOnInit Count` | `number` | -| 7 | `Initial Chunk Size` | `number` | -| 8 | `Total Chunk Count` | `number` | -| 9 | `Total Chunk Size` | `number` | -| 10 | `Lazy Chunk Count` | `number` | -| 11 | `Lazy Chunk Size` | `number` | -| 12 | `Asset Count` | `number` | -| 13 | `Asset Size` | `number` | -| 14 | ` Polyfill Size` | `number` | -| 15 | ` Css Size` | `number` | +| 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` | -# Operating System and Node Version -A User Agent string is built to "fool" Google Analytics into reading the Operating System and -version fields from it. The base dimensions are used for those. - -Node version is our App ID, but a dimension is also used to get the numeric MAJOR.MINOR of node. - -# Debugging -Using `DEBUG=universal-analytics` will report all calls to the universal-analytics library, -including queuing events and sending them to the server. - -Using `DEBUG=ng:analytics` will report additional information regarding initialization and -decisions made during the usage analytics process, e.g. if the user has analytics disabled. +## Debugging -Using `DEBUG=ng:analytics:command` will show the decisions made by the command runner. +Using `NG_DEBUG=1` will enable Google Analytics debug mode, To view the debug events, in Google Analytics go to `Configure > DebugView`. -Using `DEBUG=ng:analytics:log` will show what we actually send to GA. +## Disabling Usage Analytics -See [the `debug` NPM library](https://www.npmjs.com/package/debug) for more information. - -# Disabling Usage Analytics There are 2 ways of disabling usage analytics: -1. using `ng analytics off` (or changing the global configuration file yourself). This is the same +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. If the string `"ci"` is used it will show up as a CI run (see - below). - -# CI -A special user named `ci` is used for analytics for tracking CI information. This is a convention -and is in no way enforced. - -Running on CI by default will disable analytics (because of a lack of TTY on STDIN/OUT). It can be -manually enabled using either a global configuration with a value of `ci`, or using the -`NG_CLI_ANALYTICS=ci` environment variable. + 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 d37455e68713..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](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/) -* [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](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html) -* [Azure Service Fabric App](https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-deploy-remove-applications) -* [Heroku Docker CLI](https://github.com/heroku/heroku-container-tools) -* [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](https://docs.ansible.com/ansible/latest/modules/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.io/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](https://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 211aa1dc7a5b..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 6e638298b67d..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](http://www.protractortest.org/). - -## 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 a03e9c185ecb..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](http://www.protractortest.org/). - -### 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 c6902fccaf3b..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/browserslist/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/browserslist/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 1823e8f1ba79..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/gdi2290/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 2bb0d0f28f7f..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-schule/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 ` - - diff --git a/etc/cli.angular.io/license.html b/etc/cli.angular.io/license.html deleted file mode 100644 index 0131cc303c40..000000000000 --- a/etc/cli.angular.io/license.html +++ /dev/null @@ -1,23 +0,0 @@ -
The MIT License
-
-Copyright (c) Google, Inc. All Rights Reserved.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-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 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.
-
- diff --git a/etc/cli.angular.io/main.css b/etc/cli.angular.io/main.css deleted file mode 100644 index 61b0cb066543..000000000000 --- a/etc/cli.angular.io/main.css +++ /dev/null @@ -1 +0,0 @@ -body{font-family:"Roboto",Helvetica,sans-serif}h4,h5{font-size:30px;font-weight:400;line-height:40px;margin-bottom:15px;margin-top:15px}h5{font-size:16px;font-weight:300;line-height:28px;margin-bottom:25px;max-width:300px}.mdl-demo section.section--center{max-width:920px}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__header{background-color:#f44336;box-shadow:0 2px 5px 0 rgba(0,0,0,.26)}.mdl-layout__header a{color:#fff;text-decoration:none}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:0;width:100%}.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:25px;padding-right:0}.mdl-layout__drawer-button,.top-nav-wrapper label{display:none}@media (max-width:1024px){.mdl-layout__drawer-button{display:inline-block}}.mdl-layout__drawer{margin-top:65px;height:calc(100% - 65px)}@media (max-width:1024px){.mdl-layout__drawer{margin-top:0;height:100%}}.mdl-layout-title,.mdl-layout__title{font-size:16px;line-height:28px;letter-spacing:.02em}.microsite-name{display:inline-block;font-size:20px;margin-left:8px;margin-right:30px;text-transform:uppercase;-webkit-transform:translateY(3px);transform:translateY(3px)}.mdl-navigation__link{font-size:16px;text-transform:uppercase;text-decoration:none}.mdl-navigation__link:hover,.top-nav-wrapper label:hover{background-color:#d32f2f}.top-nav-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}@media (max-width:800px){.top-nav-wrapper{display:block;position:absolute;right:0;top:0;width:100%}.top-nav-wrapper label{cursor:pointer;display:block;float:right;line-height:56px;padding:0 16px}.top-nav-wrapper nav{background:#d32f2f;clear:both;display:none;height:auto!important}.top-nav-wrapper nav a{display:block}.top-nav-wrapper .mdl-layout-spacer{display:none}input:checked+.top-nav-wrapper label{background:#d32f2f}input:checked+.top-nav-wrapper nav{display:block}}.hero-background{background:-webkit-linear-gradient(#d32f2f ,#f44336);background:linear-gradient(#d32f2f ,#f44336);color:#fff;margin-bottom:60px}.mdl-grid,.mdl-mega-footer--bottom-section .mdl-cell--9-col{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.hero-container{padding:56px 0!important}@media (max-width:830px){.hero-container{text-align:center}}.logo-container{overflow:hidden;text-align:center}@media (max-width:840px){.tagline{max-width:100%}}.mdl-button{height:45px;line-height:45px;min-width:140px;padding:0 30px}.mdl-button--primary.mdl-button--primary.mdl-button--fab,.mdl-button--primary.mdl-button--primary.mdl-button--raised{background-color:#fff;color:#b71c1c}.features-list{width:920px;margin:0 0 23px;padding:15px 200px 15px 15px}@media (max-width:840px){.features-list{padding-right:15px}}.features-list h4{color:#37474f;font-size:28px;font-weight:500;line-height:32px;margin:0 0 16px;opacity:.87}.features-list p,footer ul a{font-size:16px;line-height:30px;opacity:.87}.button-container{margin-bottom:24px!important;text-align:center}.mdl-button--accent.mdl-button--accent.mdl-button--fab,.mdl-button--accent.mdl-button--accent.mdl-button--raised{background-color:#f44336;color:#fff}.mdl-mega-footer--bottom-section .mdl-cell--9-col{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;display:-webkit-box;display:-ms-flexbox;display:flex}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{background-color:#263238;bottom:0;color:#fff;padding-top:0;right:0}footer ul{font-size:14px;font-weight:400;letter-spacing:0;line-height:24px;list-style:none;padding:0}footer ul a{color:#fff;line-height:28px;padding:0;text-decoration:none}footer ul a:hover{text-decoration:underline}@media (max-width:830px){footer ul{background-color:rgba(0,0,0,.12);padding:8px;text-align:center}}.mdl-mega-footer--bottom-section{margin-bottom:0}.mdl-mega-footer--bottom-section p{font-size:12px;margin:0;opacity:.54}.mdl-mega-footer--bottom-section a{color:#fff;font-weight:400;padding:0;text-decoration:none}.power-text{text-align:right}@media (max-width:830px){.power-text{text-align:center;width:calc(100% - 16px)}}.mdl-base{height:100vh} diff --git a/etc/cli.angular.io/material.min.css b/etc/cli.angular.io/material.min.css deleted file mode 100644 index e750f8137205..000000000000 --- a/etc/cli.angular.io/material.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/** - * material-design-lite - Material Design Components in CSS, JS and HTML - * @version v1.1.3 - * @license Apache-2.0 - * @copyright 2015 Google, Inc. - * @link https://github.com/google/material-design-lite - */ -@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%;margin:0}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:#ff4081;font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weight:700;line-height:24px;letter-spacing:0}.mdl-typography--body-2-color-contrast{opacity:.87}.mdl-typography--body-1,.mdl-typography--body-1-color-contrast{font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-color-contrast{opacity:.87}.mdl-typography--body-2-force-preferred-font,.mdl-typography--body-2-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:24px;letter-spacing:0}.mdl-typography--body-2-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--body-1-force-preferred-font,.mdl-typography--body-1-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--caption,.mdl-typography--caption-force-preferred-font{font-size:12px;font-weight:400;line-height:1;letter-spacing:0}.mdl-typography--caption-force-preferred-font{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--caption-color-contrast,.mdl-typography--caption-force-preferred-font-color-contrast{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;opacity:.54}.mdl-typography--caption-force-preferred-font-color-contrast,.mdl-typography--menu{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--menu{font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--menu-color-contrast{opacity:.87}.mdl-typography--menu-color-contrast,.mdl-typography--button,.mdl-typography--button-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--button,.mdl-typography--button-color-contrast{text-transform:uppercase}.mdl-typography--button-color-contrast{opacity:.87}.mdl-typography--text-left{text-align:left}.mdl-typography--text-right{text-align:right}.mdl-typography--text-center{text-align:center}.mdl-typography--text-justify{text-align:justify}.mdl-typography--text-nowrap{white-space:nowrap}.mdl-typography--text-lowercase{text-transform:lowercase}.mdl-typography--text-uppercase{text-transform:uppercase}.mdl-typography--text-capitalize{text-transform:capitalize}.mdl-typography--font-thin{font-weight:200!important}.mdl-typography--font-light{font-weight:300!important}.mdl-typography--font-regular{font-weight:400!important}.mdl-typography--font-medium{font-weight:500!important}.mdl-typography--font-bold{font-weight:700!important}.mdl-typography--font-black{font-weight:900!important}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}.mdl-color-text--red{color:#f44336 !important}.mdl-color--red{background-color:#f44336 !important}.mdl-color-text--red-50{color:#ffebee !important}.mdl-color--red-50{background-color:#ffebee !important}.mdl-color-text--red-100{color:#ffcdd2 !important}.mdl-color--red-100{background-color:#ffcdd2 !important}.mdl-color-text--red-200{color:#ef9a9a !important}.mdl-color--red-200{background-color:#ef9a9a !important}.mdl-color-text--red-300{color:#e57373 !important}.mdl-color--red-300{background-color:#e57373 !important}.mdl-color-text--red-400{color:#ef5350 !important}.mdl-color--red-400{background-color:#ef5350 !important}.mdl-color-text--red-500{color:#f44336 !important}.mdl-color--red-500{background-color:#f44336 !important}.mdl-color-text--red-600{color:#e53935 !important}.mdl-color--red-600{background-color:#e53935 !important}.mdl-color-text--red-700{color:#d32f2f !important}.mdl-color--red-700{background-color:#d32f2f !important}.mdl-color-text--red-800{color:#c62828 !important}.mdl-color--red-800{background-color:#c62828 !important}.mdl-color-text--red-900{color:#b71c1c !important}.mdl-color--red-900{background-color:#b71c1c !important}.mdl-color-text--red-A100{color:#ff8a80 !important}.mdl-color--red-A100{background-color:#ff8a80 !important}.mdl-color-text--red-A200{color:#ff5252 !important}.mdl-color--red-A200{background-color:#ff5252 !important}.mdl-color-text--red-A400{color:#ff1744 !important}.mdl-color--red-A400{background-color:#ff1744 !important}.mdl-color-text--red-A700{color:#d50000 !important}.mdl-color--red-A700{background-color:#d50000 !important}.mdl-color-text--pink{color:#e91e63 !important}.mdl-color--pink{background-color:#e91e63 !important}.mdl-color-text--pink-50{color:#fce4ec !important}.mdl-color--pink-50{background-color:#fce4ec !important}.mdl-color-text--pink-100{color:#f8bbd0 !important}.mdl-color--pink-100{background-color:#f8bbd0 !important}.mdl-color-text--pink-200{color:#f48fb1 !important}.mdl-color--pink-200{background-color:#f48fb1 !important}.mdl-color-text--pink-300{color:#f06292 !important}.mdl-color--pink-300{background-color:#f06292 !important}.mdl-color-text--pink-400{color:#ec407a !important}.mdl-color--pink-400{background-color:#ec407a !important}.mdl-color-text--pink-500{color:#e91e63 !important}.mdl-color--pink-500{background-color:#e91e63 !important}.mdl-color-text--pink-600{color:#d81b60 !important}.mdl-color--pink-600{background-color:#d81b60 !important}.mdl-color-text--pink-700{color:#c2185b !important}.mdl-color--pink-700{background-color:#c2185b !important}.mdl-color-text--pink-800{color:#ad1457 !important}.mdl-color--pink-800{background-color:#ad1457 !important}.mdl-color-text--pink-900{color:#880e4f !important}.mdl-color--pink-900{background-color:#880e4f !important}.mdl-color-text--pink-A100{color:#ff80ab !important}.mdl-color--pink-A100{background-color:#ff80ab !important}.mdl-color-text--pink-A200{color:#ff4081 !important}.mdl-color--pink-A200{background-color:#ff4081 !important}.mdl-color-text--pink-A400{color:#f50057 !important}.mdl-color--pink-A400{background-color:#f50057 !important}.mdl-color-text--pink-A700{color:#c51162 !important}.mdl-color--pink-A700{background-color:#c51162 !important}.mdl-color-text--purple{color:#9c27b0 !important}.mdl-color--purple{background-color:#9c27b0 !important}.mdl-color-text--purple-50{color:#f3e5f5 !important}.mdl-color--purple-50{background-color:#f3e5f5 !important}.mdl-color-text--purple-100{color:#e1bee7 !important}.mdl-color--purple-100{background-color:#e1bee7 !important}.mdl-color-text--purple-200{color:#ce93d8 !important}.mdl-color--purple-200{background-color:#ce93d8 !important}.mdl-color-text--purple-300{color:#ba68c8 !important}.mdl-color--purple-300{background-color:#ba68c8 !important}.mdl-color-text--purple-400{color:#ab47bc !important}.mdl-color--purple-400{background-color:#ab47bc !important}.mdl-color-text--purple-500{color:#9c27b0 !important}.mdl-color--purple-500{background-color:#9c27b0 !important}.mdl-color-text--purple-600{color:#8e24aa !important}.mdl-color--purple-600{background-color:#8e24aa !important}.mdl-color-text--purple-700{color:#7b1fa2 !important}.mdl-color--purple-700{background-color:#7b1fa2 !important}.mdl-color-text--purple-800{color:#6a1b9a !important}.mdl-color--purple-800{background-color:#6a1b9a !important}.mdl-color-text--purple-900{color:#4a148c !important}.mdl-color--purple-900{background-color:#4a148c !important}.mdl-color-text--purple-A100{color:#ea80fc !important}.mdl-color--purple-A100{background-color:#ea80fc !important}.mdl-color-text--purple-A200{color:#e040fb !important}.mdl-color--purple-A200{background-color:#e040fb !important}.mdl-color-text--purple-A400{color:#d500f9 !important}.mdl-color--purple-A400{background-color:#d500f9 !important}.mdl-color-text--purple-A700{color:#a0f !important}.mdl-color--purple-A700{background-color:#a0f !important}.mdl-color-text--deep-purple{color:#673ab7 !important}.mdl-color--deep-purple{background-color:#673ab7 !important}.mdl-color-text--deep-purple-50{color:#ede7f6 !important}.mdl-color--deep-purple-50{background-color:#ede7f6 !important}.mdl-color-text--deep-purple-100{color:#d1c4e9 !important}.mdl-color--deep-purple-100{background-color:#d1c4e9 !important}.mdl-color-text--deep-purple-200{color:#b39ddb !important}.mdl-color--deep-purple-200{background-color:#b39ddb !important}.mdl-color-text--deep-purple-300{color:#9575cd !important}.mdl-color--deep-purple-300{background-color:#9575cd !important}.mdl-color-text--deep-purple-400{color:#7e57c2 !important}.mdl-color--deep-purple-400{background-color:#7e57c2 !important}.mdl-color-text--deep-purple-500{color:#673ab7 !important}.mdl-color--deep-purple-500{background-color:#673ab7 !important}.mdl-color-text--deep-purple-600{color:#5e35b1 !important}.mdl-color--deep-purple-600{background-color:#5e35b1 !important}.mdl-color-text--deep-purple-700{color:#512da8 !important}.mdl-color--deep-purple-700{background-color:#512da8 !important}.mdl-color-text--deep-purple-800{color:#4527a0 !important}.mdl-color--deep-purple-800{background-color:#4527a0 !important}.mdl-color-text--deep-purple-900{color:#311b92 !important}.mdl-color--deep-purple-900{background-color:#311b92 !important}.mdl-color-text--deep-purple-A100{color:#b388ff !important}.mdl-color--deep-purple-A100{background-color:#b388ff !important}.mdl-color-text--deep-purple-A200{color:#7c4dff !important}.mdl-color--deep-purple-A200{background-color:#7c4dff !important}.mdl-color-text--deep-purple-A400{color:#651fff !important}.mdl-color--deep-purple-A400{background-color:#651fff !important}.mdl-color-text--deep-purple-A700{color:#6200ea !important}.mdl-color--deep-purple-A700{background-color:#6200ea !important}.mdl-color-text--indigo{color:#3f51b5 !important}.mdl-color--indigo{background-color:#3f51b5 !important}.mdl-color-text--indigo-50{color:#e8eaf6 !important}.mdl-color--indigo-50{background-color:#e8eaf6 !important}.mdl-color-text--indigo-100{color:#c5cae9 !important}.mdl-color--indigo-100{background-color:#c5cae9 !important}.mdl-color-text--indigo-200{color:#9fa8da !important}.mdl-color--indigo-200{background-color:#9fa8da !important}.mdl-color-text--indigo-300{color:#7986cb !important}.mdl-color--indigo-300{background-color:#7986cb !important}.mdl-color-text--indigo-400{color:#5c6bc0 !important}.mdl-color--indigo-400{background-color:#5c6bc0 !important}.mdl-color-text--indigo-500{color:#3f51b5 !important}.mdl-color--indigo-500{background-color:#3f51b5 !important}.mdl-color-text--indigo-600{color:#3949ab !important}.mdl-color--indigo-600{background-color:#3949ab !important}.mdl-color-text--indigo-700{color:#303f9f !important}.mdl-color--indigo-700{background-color:#303f9f !important}.mdl-color-text--indigo-800{color:#283593 !important}.mdl-color--indigo-800{background-color:#283593 !important}.mdl-color-text--indigo-900{color:#1a237e !important}.mdl-color--indigo-900{background-color:#1a237e !important}.mdl-color-text--indigo-A100{color:#8c9eff !important}.mdl-color--indigo-A100{background-color:#8c9eff !important}.mdl-color-text--indigo-A200{color:#536dfe !important}.mdl-color--indigo-A200{background-color:#536dfe !important}.mdl-color-text--indigo-A400{color:#3d5afe !important}.mdl-color--indigo-A400{background-color:#3d5afe !important}.mdl-color-text--indigo-A700{color:#304ffe !important}.mdl-color--indigo-A700{background-color:#304ffe !important}.mdl-color-text--blue{color:#2196f3 !important}.mdl-color--blue{background-color:#2196f3 !important}.mdl-color-text--blue-50{color:#e3f2fd !important}.mdl-color--blue-50{background-color:#e3f2fd !important}.mdl-color-text--blue-100{color:#bbdefb !important}.mdl-color--blue-100{background-color:#bbdefb !important}.mdl-color-text--blue-200{color:#90caf9 !important}.mdl-color--blue-200{background-color:#90caf9 !important}.mdl-color-text--blue-300{color:#64b5f6 !important}.mdl-color--blue-300{background-color:#64b5f6 !important}.mdl-color-text--blue-400{color:#42a5f5 !important}.mdl-color--blue-400{background-color:#42a5f5 !important}.mdl-color-text--blue-500{color:#2196f3 !important}.mdl-color--blue-500{background-color:#2196f3 !important}.mdl-color-text--blue-600{color:#1e88e5 !important}.mdl-color--blue-600{background-color:#1e88e5 !important}.mdl-color-text--blue-700{color:#1976d2 !important}.mdl-color--blue-700{background-color:#1976d2 !important}.mdl-color-text--blue-800{color:#1565c0 !important}.mdl-color--blue-800{background-color:#1565c0 !important}.mdl-color-text--blue-900{color:#0d47a1 !important}.mdl-color--blue-900{background-color:#0d47a1 !important}.mdl-color-text--blue-A100{color:#82b1ff !important}.mdl-color--blue-A100{background-color:#82b1ff !important}.mdl-color-text--blue-A200{color:#448aff !important}.mdl-color--blue-A200{background-color:#448aff !important}.mdl-color-text--blue-A400{color:#2979ff !important}.mdl-color--blue-A400{background-color:#2979ff !important}.mdl-color-text--blue-A700{color:#2962ff !important}.mdl-color--blue-A700{background-color:#2962ff !important}.mdl-color-text--light-blue{color:#03a9f4 !important}.mdl-color--light-blue{background-color:#03a9f4 !important}.mdl-color-text--light-blue-50{color:#e1f5fe !important}.mdl-color--light-blue-50{background-color:#e1f5fe !important}.mdl-color-text--light-blue-100{color:#b3e5fc !important}.mdl-color--light-blue-100{background-color:#b3e5fc !important}.mdl-color-text--light-blue-200{color:#81d4fa !important}.mdl-color--light-blue-200{background-color:#81d4fa !important}.mdl-color-text--light-blue-300{color:#4fc3f7 !important}.mdl-color--light-blue-300{background-color:#4fc3f7 !important}.mdl-color-text--light-blue-400{color:#29b6f6 !important}.mdl-color--light-blue-400{background-color:#29b6f6 !important}.mdl-color-text--light-blue-500{color:#03a9f4 !important}.mdl-color--light-blue-500{background-color:#03a9f4 !important}.mdl-color-text--light-blue-600{color:#039be5 !important}.mdl-color--light-blue-600{background-color:#039be5 !important}.mdl-color-text--light-blue-700{color:#0288d1 !important}.mdl-color--light-blue-700{background-color:#0288d1 !important}.mdl-color-text--light-blue-800{color:#0277bd !important}.mdl-color--light-blue-800{background-color:#0277bd !important}.mdl-color-text--light-blue-900{color:#01579b !important}.mdl-color--light-blue-900{background-color:#01579b !important}.mdl-color-text--light-blue-A100{color:#80d8ff !important}.mdl-color--light-blue-A100{background-color:#80d8ff !important}.mdl-color-text--light-blue-A200{color:#40c4ff !important}.mdl-color--light-blue-A200{background-color:#40c4ff !important}.mdl-color-text--light-blue-A400{color:#00b0ff !important}.mdl-color--light-blue-A400{background-color:#00b0ff !important}.mdl-color-text--light-blue-A700{color:#0091ea !important}.mdl-color--light-blue-A700{background-color:#0091ea !important}.mdl-color-text--cyan{color:#00bcd4 !important}.mdl-color--cyan{background-color:#00bcd4 !important}.mdl-color-text--cyan-50{color:#e0f7fa !important}.mdl-color--cyan-50{background-color:#e0f7fa !important}.mdl-color-text--cyan-100{color:#b2ebf2 !important}.mdl-color--cyan-100{background-color:#b2ebf2 !important}.mdl-color-text--cyan-200{color:#80deea !important}.mdl-color--cyan-200{background-color:#80deea !important}.mdl-color-text--cyan-300{color:#4dd0e1 !important}.mdl-color--cyan-300{background-color:#4dd0e1 !important}.mdl-color-text--cyan-400{color:#26c6da !important}.mdl-color--cyan-400{background-color:#26c6da !important}.mdl-color-text--cyan-500{color:#00bcd4 !important}.mdl-color--cyan-500{background-color:#00bcd4 !important}.mdl-color-text--cyan-600{color:#00acc1 !important}.mdl-color--cyan-600{background-color:#00acc1 !important}.mdl-color-text--cyan-700{color:#0097a7 !important}.mdl-color--cyan-700{background-color:#0097a7 !important}.mdl-color-text--cyan-800{color:#00838f !important}.mdl-color--cyan-800{background-color:#00838f !important}.mdl-color-text--cyan-900{color:#006064 !important}.mdl-color--cyan-900{background-color:#006064 !important}.mdl-color-text--cyan-A100{color:#84ffff !important}.mdl-color--cyan-A100{background-color:#84ffff !important}.mdl-color-text--cyan-A200{color:#18ffff !important}.mdl-color--cyan-A200{background-color:#18ffff !important}.mdl-color-text--cyan-A400{color:#00e5ff !important}.mdl-color--cyan-A400{background-color:#00e5ff !important}.mdl-color-text--cyan-A700{color:#00b8d4 !important}.mdl-color--cyan-A700{background-color:#00b8d4 !important}.mdl-color-text--teal{color:#009688 !important}.mdl-color--teal{background-color:#009688 !important}.mdl-color-text--teal-50{color:#e0f2f1 !important}.mdl-color--teal-50{background-color:#e0f2f1 !important}.mdl-color-text--teal-100{color:#b2dfdb !important}.mdl-color--teal-100{background-color:#b2dfdb !important}.mdl-color-text--teal-200{color:#80cbc4 !important}.mdl-color--teal-200{background-color:#80cbc4 !important}.mdl-color-text--teal-300{color:#4db6ac !important}.mdl-color--teal-300{background-color:#4db6ac !important}.mdl-color-text--teal-400{color:#26a69a !important}.mdl-color--teal-400{background-color:#26a69a !important}.mdl-color-text--teal-500{color:#009688 !important}.mdl-color--teal-500{background-color:#009688 !important}.mdl-color-text--teal-600{color:#00897b !important}.mdl-color--teal-600{background-color:#00897b !important}.mdl-color-text--teal-700{color:#00796b !important}.mdl-color--teal-700{background-color:#00796b !important}.mdl-color-text--teal-800{color:#00695c !important}.mdl-color--teal-800{background-color:#00695c !important}.mdl-color-text--teal-900{color:#004d40 !important}.mdl-color--teal-900{background-color:#004d40 !important}.mdl-color-text--teal-A100{color:#a7ffeb !important}.mdl-color--teal-A100{background-color:#a7ffeb !important}.mdl-color-text--teal-A200{color:#64ffda !important}.mdl-color--teal-A200{background-color:#64ffda !important}.mdl-color-text--teal-A400{color:#1de9b6 !important}.mdl-color--teal-A400{background-color:#1de9b6 !important}.mdl-color-text--teal-A700{color:#00bfa5 !important}.mdl-color--teal-A700{background-color:#00bfa5 !important}.mdl-color-text--green{color:#4caf50 !important}.mdl-color--green{background-color:#4caf50 !important}.mdl-color-text--green-50{color:#e8f5e9 !important}.mdl-color--green-50{background-color:#e8f5e9 !important}.mdl-color-text--green-100{color:#c8e6c9 !important}.mdl-color--green-100{background-color:#c8e6c9 !important}.mdl-color-text--green-200{color:#a5d6a7 !important}.mdl-color--green-200{background-color:#a5d6a7 !important}.mdl-color-text--green-300{color:#81c784 !important}.mdl-color--green-300{background-color:#81c784 !important}.mdl-color-text--green-400{color:#66bb6a !important}.mdl-color--green-400{background-color:#66bb6a !important}.mdl-color-text--green-500{color:#4caf50 !important}.mdl-color--green-500{background-color:#4caf50 !important}.mdl-color-text--green-600{color:#43a047 !important}.mdl-color--green-600{background-color:#43a047 !important}.mdl-color-text--green-700{color:#388e3c !important}.mdl-color--green-700{background-color:#388e3c !important}.mdl-color-text--green-800{color:#2e7d32 !important}.mdl-color--green-800{background-color:#2e7d32 !important}.mdl-color-text--green-900{color:#1b5e20 !important}.mdl-color--green-900{background-color:#1b5e20 !important}.mdl-color-text--green-A100{color:#b9f6ca !important}.mdl-color--green-A100{background-color:#b9f6ca !important}.mdl-color-text--green-A200{color:#69f0ae !important}.mdl-color--green-A200{background-color:#69f0ae !important}.mdl-color-text--green-A400{color:#00e676 !important}.mdl-color--green-A400{background-color:#00e676 !important}.mdl-color-text--green-A700{color:#00c853 !important}.mdl-color--green-A700{background-color:#00c853 !important}.mdl-color-text--light-green{color:#8bc34a !important}.mdl-color--light-green{background-color:#8bc34a !important}.mdl-color-text--light-green-50{color:#f1f8e9 !important}.mdl-color--light-green-50{background-color:#f1f8e9 !important}.mdl-color-text--light-green-100{color:#dcedc8 !important}.mdl-color--light-green-100{background-color:#dcedc8 !important}.mdl-color-text--light-green-200{color:#c5e1a5 !important}.mdl-color--light-green-200{background-color:#c5e1a5 !important}.mdl-color-text--light-green-300{color:#aed581 !important}.mdl-color--light-green-300{background-color:#aed581 !important}.mdl-color-text--light-green-400{color:#9ccc65 !important}.mdl-color--light-green-400{background-color:#9ccc65 !important}.mdl-color-text--light-green-500{color:#8bc34a !important}.mdl-color--light-green-500{background-color:#8bc34a !important}.mdl-color-text--light-green-600{color:#7cb342 !important}.mdl-color--light-green-600{background-color:#7cb342 !important}.mdl-color-text--light-green-700{color:#689f38 !important}.mdl-color--light-green-700{background-color:#689f38 !important}.mdl-color-text--light-green-800{color:#558b2f !important}.mdl-color--light-green-800{background-color:#558b2f !important}.mdl-color-text--light-green-900{color:#33691e !important}.mdl-color--light-green-900{background-color:#33691e !important}.mdl-color-text--light-green-A100{color:#ccff90 !important}.mdl-color--light-green-A100{background-color:#ccff90 !important}.mdl-color-text--light-green-A200{color:#b2ff59 !important}.mdl-color--light-green-A200{background-color:#b2ff59 !important}.mdl-color-text--light-green-A400{color:#76ff03 !important}.mdl-color--light-green-A400{background-color:#76ff03 !important}.mdl-color-text--light-green-A700{color:#64dd17 !important}.mdl-color--light-green-A700{background-color:#64dd17 !important}.mdl-color-text--lime{color:#cddc39 !important}.mdl-color--lime{background-color:#cddc39 !important}.mdl-color-text--lime-50{color:#f9fbe7 !important}.mdl-color--lime-50{background-color:#f9fbe7 !important}.mdl-color-text--lime-100{color:#f0f4c3 !important}.mdl-color--lime-100{background-color:#f0f4c3 !important}.mdl-color-text--lime-200{color:#e6ee9c !important}.mdl-color--lime-200{background-color:#e6ee9c !important}.mdl-color-text--lime-300{color:#dce775 !important}.mdl-color--lime-300{background-color:#dce775 !important}.mdl-color-text--lime-400{color:#d4e157 !important}.mdl-color--lime-400{background-color:#d4e157 !important}.mdl-color-text--lime-500{color:#cddc39 !important}.mdl-color--lime-500{background-color:#cddc39 !important}.mdl-color-text--lime-600{color:#c0ca33 !important}.mdl-color--lime-600{background-color:#c0ca33 !important}.mdl-color-text--lime-700{color:#afb42b !important}.mdl-color--lime-700{background-color:#afb42b !important}.mdl-color-text--lime-800{color:#9e9d24 !important}.mdl-color--lime-800{background-color:#9e9d24 !important}.mdl-color-text--lime-900{color:#827717 !important}.mdl-color--lime-900{background-color:#827717 !important}.mdl-color-text--lime-A100{color:#f4ff81 !important}.mdl-color--lime-A100{background-color:#f4ff81 !important}.mdl-color-text--lime-A200{color:#eeff41 !important}.mdl-color--lime-A200{background-color:#eeff41 !important}.mdl-color-text--lime-A400{color:#c6ff00 !important}.mdl-color--lime-A400{background-color:#c6ff00 !important}.mdl-color-text--lime-A700{color:#aeea00 !important}.mdl-color--lime-A700{background-color:#aeea00 !important}.mdl-color-text--yellow{color:#ffeb3b !important}.mdl-color--yellow{background-color:#ffeb3b !important}.mdl-color-text--yellow-50{color:#fffde7 !important}.mdl-color--yellow-50{background-color:#fffde7 !important}.mdl-color-text--yellow-100{color:#fff9c4 !important}.mdl-color--yellow-100{background-color:#fff9c4 !important}.mdl-color-text--yellow-200{color:#fff59d !important}.mdl-color--yellow-200{background-color:#fff59d !important}.mdl-color-text--yellow-300{color:#fff176 !important}.mdl-color--yellow-300{background-color:#fff176 !important}.mdl-color-text--yellow-400{color:#ffee58 !important}.mdl-color--yellow-400{background-color:#ffee58 !important}.mdl-color-text--yellow-500{color:#ffeb3b !important}.mdl-color--yellow-500{background-color:#ffeb3b !important}.mdl-color-text--yellow-600{color:#fdd835 !important}.mdl-color--yellow-600{background-color:#fdd835 !important}.mdl-color-text--yellow-700{color:#fbc02d !important}.mdl-color--yellow-700{background-color:#fbc02d !important}.mdl-color-text--yellow-800{color:#f9a825 !important}.mdl-color--yellow-800{background-color:#f9a825 !important}.mdl-color-text--yellow-900{color:#f57f17 !important}.mdl-color--yellow-900{background-color:#f57f17 !important}.mdl-color-text--yellow-A100{color:#ffff8d !important}.mdl-color--yellow-A100{background-color:#ffff8d !important}.mdl-color-text--yellow-A200{color:#ff0 !important}.mdl-color--yellow-A200{background-color:#ff0 !important}.mdl-color-text--yellow-A400{color:#ffea00 !important}.mdl-color--yellow-A400{background-color:#ffea00 !important}.mdl-color-text--yellow-A700{color:#ffd600 !important}.mdl-color--yellow-A700{background-color:#ffd600 !important}.mdl-color-text--amber{color:#ffc107 !important}.mdl-color--amber{background-color:#ffc107 !important}.mdl-color-text--amber-50{color:#fff8e1 !important}.mdl-color--amber-50{background-color:#fff8e1 !important}.mdl-color-text--amber-100{color:#ffecb3 !important}.mdl-color--amber-100{background-color:#ffecb3 !important}.mdl-color-text--amber-200{color:#ffe082 !important}.mdl-color--amber-200{background-color:#ffe082 !important}.mdl-color-text--amber-300{color:#ffd54f !important}.mdl-color--amber-300{background-color:#ffd54f !important}.mdl-color-text--amber-400{color:#ffca28 !important}.mdl-color--amber-400{background-color:#ffca28 !important}.mdl-color-text--amber-500{color:#ffc107 !important}.mdl-color--amber-500{background-color:#ffc107 !important}.mdl-color-text--amber-600{color:#ffb300 !important}.mdl-color--amber-600{background-color:#ffb300 !important}.mdl-color-text--amber-700{color:#ffa000 !important}.mdl-color--amber-700{background-color:#ffa000 !important}.mdl-color-text--amber-800{color:#ff8f00 !important}.mdl-color--amber-800{background-color:#ff8f00 !important}.mdl-color-text--amber-900{color:#ff6f00 !important}.mdl-color--amber-900{background-color:#ff6f00 !important}.mdl-color-text--amber-A100{color:#ffe57f !important}.mdl-color--amber-A100{background-color:#ffe57f !important}.mdl-color-text--amber-A200{color:#ffd740 !important}.mdl-color--amber-A200{background-color:#ffd740 !important}.mdl-color-text--amber-A400{color:#ffc400 !important}.mdl-color--amber-A400{background-color:#ffc400 !important}.mdl-color-text--amber-A700{color:#ffab00 !important}.mdl-color--amber-A700{background-color:#ffab00 !important}.mdl-color-text--orange{color:#ff9800 !important}.mdl-color--orange{background-color:#ff9800 !important}.mdl-color-text--orange-50{color:#fff3e0 !important}.mdl-color--orange-50{background-color:#fff3e0 !important}.mdl-color-text--orange-100{color:#ffe0b2 !important}.mdl-color--orange-100{background-color:#ffe0b2 !important}.mdl-color-text--orange-200{color:#ffcc80 !important}.mdl-color--orange-200{background-color:#ffcc80 !important}.mdl-color-text--orange-300{color:#ffb74d !important}.mdl-color--orange-300{background-color:#ffb74d !important}.mdl-color-text--orange-400{color:#ffa726 !important}.mdl-color--orange-400{background-color:#ffa726 !important}.mdl-color-text--orange-500{color:#ff9800 !important}.mdl-color--orange-500{background-color:#ff9800 !important}.mdl-color-text--orange-600{color:#fb8c00 !important}.mdl-color--orange-600{background-color:#fb8c00 !important}.mdl-color-text--orange-700{color:#f57c00 !important}.mdl-color--orange-700{background-color:#f57c00 !important}.mdl-color-text--orange-800{color:#ef6c00 !important}.mdl-color--orange-800{background-color:#ef6c00 !important}.mdl-color-text--orange-900{color:#e65100 !important}.mdl-color--orange-900{background-color:#e65100 !important}.mdl-color-text--orange-A100{color:#ffd180 !important}.mdl-color--orange-A100{background-color:#ffd180 !important}.mdl-color-text--orange-A200{color:#ffab40 !important}.mdl-color--orange-A200{background-color:#ffab40 !important}.mdl-color-text--orange-A400{color:#ff9100 !important}.mdl-color--orange-A400{background-color:#ff9100 !important}.mdl-color-text--orange-A700{color:#ff6d00 !important}.mdl-color--orange-A700{background-color:#ff6d00 !important}.mdl-color-text--deep-orange{color:#ff5722 !important}.mdl-color--deep-orange{background-color:#ff5722 !important}.mdl-color-text--deep-orange-50{color:#fbe9e7 !important}.mdl-color--deep-orange-50{background-color:#fbe9e7 !important}.mdl-color-text--deep-orange-100{color:#ffccbc !important}.mdl-color--deep-orange-100{background-color:#ffccbc !important}.mdl-color-text--deep-orange-200{color:#ffab91 !important}.mdl-color--deep-orange-200{background-color:#ffab91 !important}.mdl-color-text--deep-orange-300{color:#ff8a65 !important}.mdl-color--deep-orange-300{background-color:#ff8a65 !important}.mdl-color-text--deep-orange-400{color:#ff7043 !important}.mdl-color--deep-orange-400{background-color:#ff7043 !important}.mdl-color-text--deep-orange-500{color:#ff5722 !important}.mdl-color--deep-orange-500{background-color:#ff5722 !important}.mdl-color-text--deep-orange-600{color:#f4511e !important}.mdl-color--deep-orange-600{background-color:#f4511e !important}.mdl-color-text--deep-orange-700{color:#e64a19 !important}.mdl-color--deep-orange-700{background-color:#e64a19 !important}.mdl-color-text--deep-orange-800{color:#d84315 !important}.mdl-color--deep-orange-800{background-color:#d84315 !important}.mdl-color-text--deep-orange-900{color:#bf360c !important}.mdl-color--deep-orange-900{background-color:#bf360c !important}.mdl-color-text--deep-orange-A100{color:#ff9e80 !important}.mdl-color--deep-orange-A100{background-color:#ff9e80 !important}.mdl-color-text--deep-orange-A200{color:#ff6e40 !important}.mdl-color--deep-orange-A200{background-color:#ff6e40 !important}.mdl-color-text--deep-orange-A400{color:#ff3d00 !important}.mdl-color--deep-orange-A400{background-color:#ff3d00 !important}.mdl-color-text--deep-orange-A700{color:#dd2c00 !important}.mdl-color--deep-orange-A700{background-color:#dd2c00 !important}.mdl-color-text--brown{color:#795548 !important}.mdl-color--brown{background-color:#795548 !important}.mdl-color-text--brown-50{color:#efebe9 !important}.mdl-color--brown-50{background-color:#efebe9 !important}.mdl-color-text--brown-100{color:#d7ccc8 !important}.mdl-color--brown-100{background-color:#d7ccc8 !important}.mdl-color-text--brown-200{color:#bcaaa4 !important}.mdl-color--brown-200{background-color:#bcaaa4 !important}.mdl-color-text--brown-300{color:#a1887f !important}.mdl-color--brown-300{background-color:#a1887f !important}.mdl-color-text--brown-400{color:#8d6e63 !important}.mdl-color--brown-400{background-color:#8d6e63 !important}.mdl-color-text--brown-500{color:#795548 !important}.mdl-color--brown-500{background-color:#795548 !important}.mdl-color-text--brown-600{color:#6d4c41 !important}.mdl-color--brown-600{background-color:#6d4c41 !important}.mdl-color-text--brown-700{color:#5d4037 !important}.mdl-color--brown-700{background-color:#5d4037 !important}.mdl-color-text--brown-800{color:#4e342e !important}.mdl-color--brown-800{background-color:#4e342e !important}.mdl-color-text--brown-900{color:#3e2723 !important}.mdl-color--brown-900{background-color:#3e2723 !important}.mdl-color-text--grey{color:#9e9e9e !important}.mdl-color--grey{background-color:#9e9e9e !important}.mdl-color-text--grey-50{color:#fafafa !important}.mdl-color--grey-50{background-color:#fafafa !important}.mdl-color-text--grey-100{color:#f5f5f5 !important}.mdl-color--grey-100{background-color:#f5f5f5 !important}.mdl-color-text--grey-200{color:#eee !important}.mdl-color--grey-200{background-color:#eee !important}.mdl-color-text--grey-300{color:#e0e0e0 !important}.mdl-color--grey-300{background-color:#e0e0e0 !important}.mdl-color-text--grey-400{color:#bdbdbd !important}.mdl-color--grey-400{background-color:#bdbdbd !important}.mdl-color-text--grey-500{color:#9e9e9e !important}.mdl-color--grey-500{background-color:#9e9e9e !important}.mdl-color-text--grey-600{color:#757575 !important}.mdl-color--grey-600{background-color:#757575 !important}.mdl-color-text--grey-700{color:#616161 !important}.mdl-color--grey-700{background-color:#616161 !important}.mdl-color-text--grey-800{color:#424242 !important}.mdl-color--grey-800{background-color:#424242 !important}.mdl-color-text--grey-900{color:#212121 !important}.mdl-color--grey-900{background-color:#212121 !important}.mdl-color-text--blue-grey{color:#607d8b !important}.mdl-color--blue-grey{background-color:#607d8b !important}.mdl-color-text--blue-grey-50{color:#eceff1 !important}.mdl-color--blue-grey-50{background-color:#eceff1 !important}.mdl-color-text--blue-grey-100{color:#cfd8dc !important}.mdl-color--blue-grey-100{background-color:#cfd8dc !important}.mdl-color-text--blue-grey-200{color:#b0bec5 !important}.mdl-color--blue-grey-200{background-color:#b0bec5 !important}.mdl-color-text--blue-grey-300{color:#90a4ae !important}.mdl-color--blue-grey-300{background-color:#90a4ae !important}.mdl-color-text--blue-grey-400{color:#78909c !important}.mdl-color--blue-grey-400{background-color:#78909c !important}.mdl-color-text--blue-grey-500{color:#607d8b !important}.mdl-color--blue-grey-500{background-color:#607d8b !important}.mdl-color-text--blue-grey-600{color:#546e7a !important}.mdl-color--blue-grey-600{background-color:#546e7a !important}.mdl-color-text--blue-grey-700{color:#455a64 !important}.mdl-color--blue-grey-700{background-color:#455a64 !important}.mdl-color-text--blue-grey-800{color:#37474f !important}.mdl-color--blue-grey-800{background-color:#37474f !important}.mdl-color-text--blue-grey-900{color:#263238 !important}.mdl-color--blue-grey-900{background-color:#263238 !important}.mdl-color--black{background-color:#000 !important}.mdl-color-text--black{color:#000 !important}.mdl-color--white{background-color:#fff !important}.mdl-color-text--white{color:#fff !important}.mdl-color--primary{background-color:#3f51b5 !important}.mdl-color--primary-contrast{background-color:#fff !important}.mdl-color--primary-dark{background-color:#303f9f !important}.mdl-color--accent{background-color:#ff4081 !important}.mdl-color--accent-contrast{background-color:#fff !important}.mdl-color-text--primary{color:#3f51b5 !important}.mdl-color-text--primary-contrast{color:#fff !important}.mdl-color-text--primary-dark{color:#303f9f !important}.mdl-color-text--accent{color:#ff4081 !important}.mdl-color-text--accent-contrast{color:#fff !important}.mdl-ripple{background:#000;border-radius:50%;height:50px;left:0;opacity:0;pointer-events:none;position:absolute;top:0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:50px;overflow:hidden}.mdl-ripple.is-animating{transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1);transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1),-webkit-transform .3s cubic-bezier(0,0,.2,1)}.mdl-ripple.is-visible{opacity:.3}.mdl-animation--default,.mdl-animation--fast-out-slow-in{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-animation--linear-out-slow-in{transition-timing-function:cubic-bezier(0,0,.2,1)}.mdl-animation--fast-out-linear-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.mdl-badge{position:relative;white-space:nowrap;margin-right:24px}.mdl-badge:not([data-badge]){margin-right:auto}.mdl-badge[data-badge]:after{content:attr(data-badge);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;position:absolute;top:-11px;right:-24px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:600;font-size:12px;width:22px;height:22px;border-radius:50%;background:#ff4081;color:#fff}.mdl-button .mdl-badge[data-badge]:after{top:-10px;right:-5px}.mdl-badge.mdl-badge--no-background[data-badge]:after{color:#ff4081;background:rgba(255,255,255,.2);box-shadow:0 0 1px gray}.mdl-badge.mdl-badge--overlap{margin-right:10px}.mdl-badge.mdl-badge--overlap:after{right:-10px}.mdl-button{background:0 0;border:none;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);outline:none;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:#3f51b5}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type="submit"]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:#3f51b5;color:#fff}.mdl-button--raised.mdl-button--colored:hover{background-color:#3f51b5}.mdl-button--raised.mdl-button--colored:active{background-color:#3f51b5}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:#3f51b5}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:#fff}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:#ff4081;color:#fff}.mdl-button--fab.mdl-button--colored:hover{background-color:#ff4081}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:#ff4081}.mdl-button--fab.mdl-button--colored:active{background-color:#ff4081}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:#fff}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:#3f51b5}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:#fff}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:#fff;background-color:#3f51b5}.mdl-button--accent.mdl-button--accent{color:#ff4081}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:#fff}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:#fff;background-color:#ff4081}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-card{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;font-size:16px;font-weight:400;min-height:200px;overflow:hidden;width:330px;z-index:1;position:relative;background:#fff;border-radius:2px;box-sizing:border-box}.mdl-card__media{background-color:#ff4081;background-repeat:repeat;background-position:50% 50%;background-size:cover;background-origin:padding-box;background-attachment:scroll;box-sizing:border-box}.mdl-card__title{-webkit-align-items:center;-ms-flex-align:center;align-items:center;color:#000;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:stretch;-ms-flex-pack:stretch;justify-content:stretch;line-height:normal;padding:16px;-webkit-perspective-origin:165px 56px;perspective-origin:165px 56px;-webkit-transform-origin:165px 56px;transform-origin:165px 56px;box-sizing:border-box}.mdl-card__title.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__title-text{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;color:inherit;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:24px;font-weight:300;line-height:normal;overflow:hidden;-webkit-transform-origin:149px 48px;transform-origin:149px 48px;margin:0}.mdl-card__subtitle-text{font-size:14px;color:rgba(0,0,0,.54);margin:0}.mdl-card__supporting-text{color:rgba(0,0,0,.54);font-size:1rem;line-height:18px;overflow:hidden;padding:16px;width:90%}.mdl-card__actions{font-size:16px;line-height:normal;width:100%;background-color:transparent;padding:8px;box-sizing:border-box}.mdl-card__actions.mdl-card--border{border-top:1px solid rgba(0,0,0,.1)}.mdl-card--expand{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-card__menu{position:absolute;right:16px;top:16px}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid #3f51b5}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(63,81,181,.26);background-color:rgba(63,81,181,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");background:0 0;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:#3f51b5 url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:#3f51b5}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0 0}.mdl-data-table{position:relative;border:1px solid rgba(0,0,0,.12);border-collapse:collapse;white-space:nowrap;font-size:13px;background-color:#fff}.mdl-data-table thead{padding-bottom:3px}.mdl-data-table thead .mdl-data-table__select{margin-top:0}.mdl-data-table tbody tr{position:relative;height:48px;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background-color}.mdl-data-table tbody tr.is-selected{background-color:#e0e0e0}.mdl-data-table tbody tr:hover{background-color:#eee}.mdl-data-table td{text-align:right}.mdl-data-table th{padding:0 18px 12px 18px;text-align:right}.mdl-data-table td:first-of-type,.mdl-data-table th:first-of-type{padding-left:24px}.mdl-data-table td:last-of-type,.mdl-data-table th:last-of-type{padding-right:24px}.mdl-data-table td{position:relative;height:48px;border-top:1px solid rgba(0,0,0,.12);border-bottom:1px solid rgba(0,0,0,.12);padding:12px 18px;box-sizing:border-box}.mdl-data-table td,.mdl-data-table td .mdl-data-table__select{vertical-align:middle}.mdl-data-table th{position:relative;vertical-align:bottom;text-overflow:ellipsis;font-weight:700;line-height:24px;letter-spacing:0;height:48px;font-size:12px;color:rgba(0,0,0,.54);padding-bottom:8px;box-sizing:border-box}.mdl-data-table th.mdl-data-table__header--sorted-ascending,.mdl-data-table th.mdl-data-table__header--sorted-descending{color:rgba(0,0,0,.87)}.mdl-data-table th.mdl-data-table__header--sorted-ascending:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:before{font-family:'Material Icons';font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-size:16px;content:"\e5d8";margin-right:5px;vertical-align:sub}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover{cursor:pointer}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before{color:rgba(0,0,0,.26)}.mdl-data-table th.mdl-data-table__header--sorted-descending:before{content:"\e5db"}.mdl-data-table__select{width:16px}.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric{text-align:left}.mdl-dialog{border:none;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2);width:280px}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-mega-footer{padding:16px 40px;color:#9e9e9e;background-color:#424242}.mdl-mega-footer--top-section:after,.mdl-mega-footer--middle-section:after,.mdl-mega-footer--bottom-section:after,.mdl-mega-footer__top-section:after,.mdl-mega-footer__middle-section:after,.mdl-mega-footer__bottom-section:after{content:'';display:block;clear:both}.mdl-mega-footer--left-section,.mdl-mega-footer__left-section,.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{margin-bottom:16px}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:block;margin-bottom:16px;color:inherit;text-decoration:none}@media screen and (min-width:760px){.mdl-mega-footer--left-section,.mdl-mega-footer__left-section{float:left}.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{float:right}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:inline-block;margin-left:16px;line-height:36px;vertical-align:middle}}.mdl-mega-footer--social-btn,.mdl-mega-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{display:block;position:relative}@media screen and (min-width:760px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{width:33%}.mdl-mega-footer--drop-down-section:nth-child(1),.mdl-mega-footer--drop-down-section:nth-child(2),.mdl-mega-footer__drop-down-section:nth-child(1),.mdl-mega-footer__drop-down-section:nth-child(2){float:left}.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(3){float:right}.mdl-mega-footer--drop-down-section:nth-child(3):after,.mdl-mega-footer__drop-down-section:nth-child(3):after{clear:right}.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section:nth-child(4){clear:right;float:right}.mdl-mega-footer--middle-section:after,.mdl-mega-footer__middle-section:after{content:'';display:block;clear:both}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:0}}@media screen and (min-width:1024px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section,.mdl-mega-footer__drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(4){width:24%;float:left}}.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{position:absolute;width:100%;height:55.8px;padding:32px;margin:-16px 0 0;cursor:pointer;z-index:1;opacity:0}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CE'}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list{display:none}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CF'}.mdl-mega-footer--heading,.mdl-mega-footer__heading{position:relative;width:100%;padding-right:39.8px;margin-bottom:16px;box-sizing:border-box;font-size:14px;line-height:23.8px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#e0e0e0}.mdl-mega-footer--heading:after,.mdl-mega-footer__heading:after{content:'';position:absolute;top:0;right:0;display:block;width:23.8px;height:23.8px;background-size:cover}.mdl-mega-footer--link-list,.mdl-mega-footer__link-list{list-style:none;padding:0;margin:0 0 32px}.mdl-mega-footer--link-list:after,.mdl-mega-footer__link-list:after{clear:both;display:block;content:''}.mdl-mega-footer--link-list li,.mdl-mega-footer__link-list li{font-size:14px;font-weight:400;letter-spacing:0;line-height:20px}.mdl-mega-footer--link-list a,.mdl-mega-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}@media screen and (min-width:760px){.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{display:none}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{content:''}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list{display:block}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{content:''}}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:16px;margin-bottom:16px}.mdl-logo{margin-bottom:16px;color:#fff}.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li,.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li{float:left;margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-logo{float:left;margin-bottom:0;margin-right:16px}}.mdl-mini-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:32px 16px;color:#9e9e9e;background-color:#424242}.mdl-mini-footer:after{content:'';display:block}.mdl-mini-footer .mdl-logo{line-height:36px}.mdl-mini-footer--link-list,.mdl-mini-footer__link-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;list-style:none;margin:0;padding:0}.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{line-height:36px}}.mdl-mini-footer--link-list a,.mdl-mini-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}.mdl-mini-footer--left-section,.mdl-mini-footer__left-section{display:inline-block;-webkit-order:0;-ms-flex-order:0;order:0}.mdl-mini-footer--right-section,.mdl-mini-footer__right-section{display:inline-block;-webkit-order:1;-ms-flex-order:1;order:1}.mdl-mini-footer--social-btn,.mdl-mini-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-icon-toggle{position:relative;z-index:1;vertical-align:middle;display:inline-block;height:32px;margin:0;padding:0}.mdl-icon-toggle__input{line-height:32px}.mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-icon-toggle__label{display:inline-block;position:relative;cursor:pointer;height:32px;width:32px;min-width:32px;color:#616161;border-radius:50%;padding:0;margin-left:0;margin-right:0;text-align:center;background-color:transparent;will-change:background-color;transition:background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1)}.mdl-icon-toggle__label.material-icons{line-height:32px;font-size:24px}.mdl-icon-toggle.is-checked .mdl-icon-toggle__label{color:#3f51b5}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__label{color:rgba(0,0,0,.26);cursor:auto;transition:none}.mdl-icon-toggle.is-focused .mdl-icon-toggle__label{background-color:rgba(0,0,0,.12)}.mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label{background-color:rgba(63,81,181,.26)}.mdl-icon-toggle__ripple-container{position:absolute;z-index:2;top:-2px;left:-2px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-icon-toggle__ripple-container .mdl-ripple{background:#616161}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container{cursor:auto}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple{background:0 0}.mdl-list{display:block;padding:8px 0;list-style:none}.mdl-list__item{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:.04em;line-height:1;min-height:48px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:16px;cursor:default;color:rgba(0,0,0,.87);overflow:hidden}.mdl-list__item,.mdl-list__item .mdl-list__item-primary-content{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.mdl-list__item .mdl-list__item-primary-content{-webkit-order:0;-ms-flex-order:0;order:0;-webkit-flex-grow:2;-ms-flex-positive:2;flex-grow:2;text-decoration:none}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon{margin-right:32px}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar{margin-right:16px}.mdl-list__item .mdl-list__item-secondary-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:column;-ms-flex-flow:column;flex-flow:column;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end;margin-left:16px}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label{display:inline}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;color:rgba(0,0,0,.54)}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header{padding:0 0 0 16px}.mdl-list__item-icon,.mdl-list__item-icon.material-icons{height:24px;width:24px;font-size:24px;box-sizing:border-box;color:#757575}.mdl-list__item-avatar,.mdl-list__item-avatar.material-icons{height:40px;width:40px;box-sizing:border-box;border-radius:50%;background-color:#757575;font-size:40px;color:#fff}.mdl-list__item--two-line{height:72px}.mdl-list__item--two-line .mdl-list__item-primary-content{height:36px;line-height:20px;display:block}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar{float:left}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left;margin-top:6px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content{height:36px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-list__item--three-line{height:88px}.mdl-list__item--three-line .mdl-list__item-primary-content{height:52px;line-height:20px;display:block}.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar,.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left}.mdl-list__item--three-line .mdl-list__item-secondary-content{height:52px}.mdl-list__item--three-line .mdl-list__item-text-body{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;height:52px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-menu__container{display:block;margin:0;padding:0;border:none;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:none;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(.4,0,.2,1),clip .3s cubic-bezier(.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:none;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0 0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:none;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0%;transition:width .2s cubic-bezier(.4,0,.2,1)}.mdl-progress>.progressbar{background-color:#3f51b5;z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,#3f51b5 ,#3f51b5);z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,#3f51b5 ,#3f51b5);-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=")}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.9),rgba(255,255,255,.9)),linear-gradient(to right,#3f51b5 ,#3f51b5)}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:#3f51b5;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@-webkit-keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}@keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}.mdl-navigation{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;box-sizing:border-box}.mdl-navigation__link{color:#424242;text-decoration:none;margin:0;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0;opacity:.87}.mdl-navigation__link .material-icons{vertical-align:middle}.mdl-layout{width:100%;height:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow-y:auto;overflow-x:hidden;position:relative;-webkit-overflow-scrolling:touch}.mdl-layout.is-small-screen .mdl-layout--large-screen-only{display:none}.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only{display:none}.mdl-layout__container{position:absolute;width:100%;height:100%}.mdl-layout__title,.mdl-layout-title{display:block;position:relative;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;line-height:1;letter-spacing:.02em;font-weight:400;box-sizing:border-box}.mdl-layout-spacer{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-layout__drawer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;width:240px;height:100%;max-height:100%;position:absolute;top:0;left:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);box-sizing:border-box;border-right:1px solid #e0e0e0;background:#fafafa;-webkit-transform:translateX(-250px);transform:translateX(-250px);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;will-change:transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;color:#424242;overflow:visible;overflow-y:auto;z-index:5}.mdl-layout__drawer.is-visible{-webkit-transform:translateX(0);transform:translateX(0)}.mdl-layout__drawer.is-visible~.mdl-layout__content.mdl-layout__content{overflow:hidden}.mdl-layout__drawer>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:64px;padding-left:40px}@media screen and (max-width:1024px){.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:56px;padding-left:16px}}.mdl-layout__drawer .mdl-navigation{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;-ms-flex-align:stretch;-ms-grid-row-align:stretch;align-items:stretch;padding-top:16px}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{display:block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:16px 40px;margin:0;color:#757575}@media screen and (max-width:1024px){.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{padding:16px}}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover{background-color:#e0e0e0}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current{background-color:#000;color:#e0e0e0}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer{-webkit-transform:translateX(0);transform:translateX(0)}}.mdl-layout__drawer-button{display:block;position:absolute;height:48px;width:48px;border:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden;text-align:center;cursor:pointer;font-size:26px;line-height:50px;font-family:Helvetica,Arial,sans-serif;margin:10px 12px;top:0;left:0;color:#fff;z-index:4}.mdl-layout__header .mdl-layout__drawer-button{position:absolute;color:#fff;background-color:inherit}@media screen and (max-width:1024px){.mdl-layout__header .mdl-layout__drawer-button{margin:4px}}@media screen and (max-width:1024px){.mdl-layout__drawer-button{margin:4px;color:rgba(0,0,0,.5)}}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer-button,.mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button{display:none}}.mdl-layout--no-drawer-button .mdl-layout__drawer-button{display:none}.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;box-sizing:border-box;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%;margin:0;padding:0;border:none;min-height:64px;max-height:1000px;z-index:3;background-color:#3f51b5;color:#fff;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:max-height,box-shadow}@media screen and (max-width:1024px){.mdl-layout__header{min-height:56px}}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:240px;width:calc(100% - 240px)}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:40px}}.mdl-layout__header>.mdl-layout-icon{position:absolute;left:40px;top:16px;height:32px;width:32px;overflow:hidden;z-index:3;display:block}@media screen and (max-width:1024px){.mdl-layout__header>.mdl-layout-icon{left:16px;top:12px}}.mdl-layout.has-drawer .mdl-layout__header>.mdl-layout-icon{display:none}.mdl-layout__header.is-compact{max-height:64px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact{max-height:56px}}.mdl-layout__header.is-compact.has-tabs{height:112px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact.has-tabs{min-height:104px}}@media screen and (max-width:1024px){.mdl-layout__header{display:none}.mdl-layout--fixed-header>.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex}}.mdl-layout__header--transparent.mdl-layout__header--transparent{background-color:transparent;box-shadow:none}.mdl-layout__header--seamed,.mdl-layout__header--scroll{box-shadow:none}.mdl-layout__header--waterfall{box-shadow:none;overflow:hidden}.mdl-layout__header--waterfall.is-casting-shadow{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.mdl-layout__header-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;box-sizing:border-box;-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:64px;margin:0;padding:0 40px 0 80px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:40px}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__header-row{padding-left:40px}}@media screen and (max-width:1024px){.mdl-layout__header-row{height:56px;padding:0 16px 0 72px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:16px}}.mdl-layout__header-row>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__header--scroll .mdl-layout__header-row{width:100%}.mdl-layout__header-row .mdl-navigation{margin:0;padding:0;height:64px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation{height:56px}}.mdl-layout__header-row .mdl-navigation__link{display:block;color:#fff;line-height:64px;padding:0 24px}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation__link{line-height:56px;padding:0 16px}}.mdl-layout__obfuscator{background-color:transparent;position:absolute;top:0;left:0;height:100%;width:100%;z-index:4;visibility:hidden;transition-property:background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-layout__obfuscator.is-visible{background-color:rgba(0,0,0,.5);visibility:visible}@supports (pointer-events:auto){.mdl-layout__obfuscator{background-color:rgba(0,0,0,.5);opacity:0;transition-property:opacity;visibility:visible;pointer-events:none}.mdl-layout__obfuscator.is-visible{pointer-events:auto;opacity:1}}.mdl-layout__content{-ms-flex:0 1 auto;position:relative;display:inline-block;overflow-y:auto;overflow-x:hidden;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;z-index:1;-webkit-overflow-scrolling:touch}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:240px}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow:visible}@media screen and (max-width:1024px){.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow-y:auto;overflow-x:hidden}}.mdl-layout__tab-bar{height:96px;margin:0;width:calc(100% - 112px);padding:0 0 0 56px;display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:#3f51b5;overflow-y:hidden;overflow-x:scroll}.mdl-layout__tab-bar::-webkit-scrollbar{display:none}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}}@media screen and (max-width:1024px){.mdl-layout__tab-bar{width:calc(100% - 60px);padding:0 0 0 60px}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{width:calc(100% - 8px);padding-left:4px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar{padding:0;overflow:hidden;width:100%}.mdl-layout__tab-bar-container{position:relative;height:48px;width:100%;border:none;margin:0;z-index:2;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden}.mdl-layout__container>.mdl-layout__tab-bar-container{position:absolute;top:0;left:0}.mdl-layout__tab-bar-button{display:inline-block;position:absolute;top:0;height:48px;width:56px;z-index:4;text-align:center;background-color:#3f51b5;color:transparent;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button{width:16px}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons{position:relative;left:-4px}@media screen and (max-width:1024px){.mdl-layout__tab-bar-button{display:none;width:60px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar-button{display:none}.mdl-layout__tab-bar-button .material-icons{line-height:48px}.mdl-layout__tab-bar-button.is-active{color:#fff}.mdl-layout__tab-bar-left-button{left:0}.mdl-layout__tab-bar-right-button{right:0}.mdl-layout__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(255,255,255,.6);overflow:hidden}@media screen and (max-width:1024px){.mdl-layout__tab{padding:0 12px}}.mdl-layout--fixed-tabs .mdl-layout__tab{float:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding:0}.mdl-layout.is-upgraded .mdl-layout__tab.is-active{color:#fff}.mdl-layout.is-upgraded .mdl-layout__tab.is-active::after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:#ff4081;-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-layout__tab .mdl-layout__tab-ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple{background-color:#fff}.mdl-layout__tab-panel{display:block}.mdl-layout.is-upgraded .mdl-layout__tab-panel{display:none}.mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active{display:block}.mdl-radio{position:relative;font-size:16px;line-height:24px;display:inline-block;box-sizing:border-box;margin:0;padding-left:0}.mdl-radio.is-upgraded{padding-left:24px}.mdl-radio__button{line-height:24px}.mdl-radio.is-upgraded .mdl-radio__button{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-radio__outer-circle{position:absolute;top:4px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;border:2px solid rgba(0,0,0,.54);border-radius:50%;z-index:2}.mdl-radio.is-checked .mdl-radio__outer-circle{border:2px solid #3f51b5}.mdl-radio__outer-circle fieldset[disabled] .mdl-radio,.mdl-radio.is-disabled .mdl-radio__outer-circle{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-radio__inner-circle{position:absolute;z-index:1;margin:0;top:8px;left:4px;box-sizing:border-box;width:8px;height:8px;cursor:pointer;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transform:scale3d(0,0,0);transform:scale3d(0,0,0);border-radius:50%;background:#3f51b5}.mdl-radio.is-checked .mdl-radio__inner-circle{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}fieldset[disabled] .mdl-radio .mdl-radio__inner-circle,.mdl-radio.is-disabled .mdl-radio__inner-circle{background:rgba(0,0,0,.26);cursor:auto}.mdl-radio.is-focused .mdl-radio__inner-circle{box-shadow:0 0 0 10px rgba(0,0,0,.1)}.mdl-radio__label{cursor:pointer}fieldset[disabled] .mdl-radio .mdl-radio__label,.mdl-radio.is-disabled .mdl-radio__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-radio__ripple-container{position:absolute;z-index:2;top:-9px;left:-13px;box-sizing:border-box;width:42px;height:42px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-radio__ripple-container .mdl-ripple{background:#3f51b5}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container,.mdl-radio.is-disabled .mdl-radio__ripple-container{cursor:auto}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple,.mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple{background:0 0}_:-ms-input-placeholder,:root .mdl-slider.mdl-slider.is-upgraded{-ms-appearance:none;height:32px;margin:0}.mdl-slider{width:calc(100% - 40px);margin:0 20px}.mdl-slider.is-upgraded{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;padding:0;color:#3f51b5;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center;z-index:1;cursor:pointer}.mdl-slider.is-upgraded::-moz-focus-outer{border:0}.mdl-slider.is-upgraded::-ms-tooltip{display:none}.mdl-slider.is-upgraded::-webkit-slider-runnable-track{background:0 0}.mdl-slider.is-upgraded::-moz-range-track{background:0 0;border:none}.mdl-slider.is-upgraded::-ms-track{background:0 0;color:transparent;height:2px;width:100%;border:none}.mdl-slider.is-upgraded::-ms-fill-lower{padding:0;background:linear-gradient(to right,transparent,transparent 16px,#3f51b5 16px,#3f51b5 0)}.mdl-slider.is-upgraded::-ms-fill-upper{padding:0;background:linear-gradient(to left,transparent,transparent 16px,rgba(0,0,0,.26)16px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background:#3f51b5;border:none;transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded::-moz-range-thumb{-moz-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background-image:none;background:#3f51b5;border:none}.mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(63,81,181,.26)}.mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(63,81,181,.26)}.mdl-slider.is-upgraded:active::-webkit-slider-thumb{background-image:none;background:#3f51b5;-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded:active::-moz-range-thumb{background-image:none;background:#3f51b5;transform:scale(1.5)}.mdl-slider.is-upgraded::-ms-thumb{width:32px;height:32px;border:none;border-radius:50%;background:#3f51b5;transform:scale(.375);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,#3f51b5 0%,#3f51b5 37.5%,rgba(63,81,181,.26)37.5%,rgba(63,81,181,.26)100%);transform:scale(1)}.mdl-slider.is-upgraded:active::-ms-thumb{background:#3f51b5;transform:scale(.5625)}.mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb{border:1.6px solid rgba(0,0,0,.26);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:9px}.mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb{border:1.5px solid rgba(0,0,0,.26);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value::-ms-thumb{background:radial-gradient(circle closest-side,transparent 0%,transparent 66.67%,rgba(0,0,0,.26)66.67%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgba(0,0,0,.12)0%,rgba(0,0,0,.12)25%,rgba(0,0,0,.26)25%,rgba(0,0,0,.26)37.5%,rgba(0,0,0,.12)37.5%,rgba(0,0,0,.12)100%);transform:scale(1)}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb{transform:scale(.5625);background:radial-gradient(circle closest-side,transparent 0%,transparent 77.78%,rgba(0,0,0,.26)77.78%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower{background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper{margin-left:9px}.mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled::-webkit-slider-thumb{-webkit-transform:scale(.667);transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded:disabled::-moz-range-thumb{transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-lower{background-color:rgba(0,0,0,.26);left:-6px}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;-webkit-transform:scale(.667);transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded:disabled:active::-ms-thumb,.mdl-slider.is-upgraded:disabled::-ms-thumb{transform:scale(.25);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb{transform:scale(.25);background:radial-gradient(circle closest-side,transparent 0%,transparent 50%,rgba(0,0,0,.26)50%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded:disabled::-ms-fill-lower{margin-right:6px;background:linear-gradient(to right,transparent,transparent 25px,rgba(0,0,0,.26)25px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded:disabled::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper{margin-left:6px}.mdl-slider__ie-container{height:18px;overflow:visible;border:none;margin:none;padding:none}.mdl-slider__container{height:18px;position:relative;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.mdl-slider__container,.mdl-slider__background-flex{background:0 0;display:-webkit-flex;display:-ms-flexbox;display:flex}.mdl-slider__background-flex{position:absolute;height:2px;width:calc(100% - 52px);top:50%;left:0;margin:0 26px;overflow:hidden;border:0;padding:0;-webkit-transform:translate(0,-1px);transform:translate(0,-1px)}.mdl-slider__background-lower{background:#3f51b5}.mdl-slider__background-lower,.mdl-slider__background-upper{-webkit-flex:0;-ms-flex:0;flex:0;position:relative;border:0;padding:0}.mdl-slider__background-upper{background:rgba(0,0,0,.26);transition:left .18s cubic-bezier(.4,0,.2,1)}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:"Roboto","Helvetica","Arial",sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(.4,0,1,1);transition:transform .25s cubic-bezier(.4,0,1,1),-webkit-transform .25s cubic-bezier(.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0 0;border:none;color:#ff4081;float:right;padding:14px 24px 14px 12px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:none;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-spinner{display:inline-block;position:relative;width:28px;height:28px}.mdl-spinner:not(.is-upgraded).is-active:after{content:"Loading..."}.mdl-spinner.is-upgraded.is-active{-webkit-animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite;animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite}@-webkit-keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.mdl-spinner__layer{position:absolute;width:100%;height:100%;opacity:0}.mdl-spinner__layer-1{border-color:#42a5f5}.mdl-spinner--single-color .mdl-spinner__layer-1{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-1{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-2{border-color:#f44336}.mdl-spinner--single-color .mdl-spinner__layer-2{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-2{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-3{border-color:#fdd835}.mdl-spinner--single-color .mdl-spinner__layer-3{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-3{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-4{border-color:#4caf50}.mdl-spinner--single-color .mdl-spinner__layer-4{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-4{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@-webkit-keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}@keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}.mdl-spinner__gap-patch{position:absolute;box-sizing:border-box;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__gap-patch .mdl-spinner__circle{width:1000%;left:-450%}.mdl-spinner__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__circle-clipper .mdl-spinner__circle{width:200%}.mdl-spinner__circle{box-sizing:border-box;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0;left:0}.mdl-spinner__left .mdl-spinner__circle{border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle{-webkit-animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__right .mdl-spinner__circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle{-webkit-animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}@keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}.mdl-switch{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0;overflow:visible;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-switch.is-upgraded{padding-left:28px}.mdl-switch__input{line-height:24px}.mdl-switch.is-upgraded .mdl-switch__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-switch__track{background:rgba(0,0,0,.26);position:absolute;left:0;top:5px;height:14px;width:36px;border-radius:14px;cursor:pointer}.mdl-switch.is-checked .mdl-switch__track{background:rgba(63,81,181,.5)}.mdl-switch__track fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__track{background:rgba(0,0,0,.12);cursor:auto}.mdl-switch__thumb{background:#fafafa;position:absolute;left:0;top:2px;height:20px;width:20px;border-radius:50%;cursor:pointer;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:left}.mdl-switch.is-checked .mdl-switch__thumb{background:#3f51b5;left:16px;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-switch__thumb fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__thumb{background:#bdbdbd;cursor:auto}.mdl-switch__focus-helper{position:absolute;top:50%;left:50%;-webkit-transform:translate(-4px,-4px);transform:translate(-4px,-4px);display:inline-block;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background-color:transparent}.mdl-switch.is-focused .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-switch.is-focused.is-checked .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(63,81,181,.26);background-color:rgba(63,81,181,.26)}.mdl-switch__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0;left:24px}.mdl-switch__label fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__label{color:#bdbdbd;cursor:auto}.mdl-switch__ripple-container{position:absolute;z-index:2;top:-12px;left:-14px;box-sizing:border-box;width:48px;height:48px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000);transition-duration:.4s;transition-timing-function:step-end;transition-property:left}.mdl-switch__ripple-container .mdl-ripple{background:#3f51b5}.mdl-switch__ripple-container fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__ripple-container{cursor:auto}fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple,.mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple{background:0 0}.mdl-switch.is-checked .mdl-switch__ripple-container{left:2px}.mdl-tabs{display:block;width:100%}.mdl-tabs__tab-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:space-between;-ms-flex-line-pack:justify;align-content:space-between;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;height:48px;padding:0;margin:0;border-bottom:1px solid #e0e0e0}.mdl-tabs__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(0,0,0,.54);overflow:hidden}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active{color:rgba(0,0,0,.87)}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:#3f51b5;-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-tabs__tab .mdl-tabs__ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple{background:#3f51b5}.mdl-tabs__panel{display:block}.mdl-tabs.is-upgraded .mdl-tabs__panel{display:none}.mdl-tabs.is-upgraded .mdl-tabs__panel.is-active{display:block}@-webkit-keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}@keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}.mdl-textfield{position:relative;font-size:16px;display:inline-block;box-sizing:border-box;width:300px;max-width:100%;margin:0;padding:20px 0}.mdl-textfield .mdl-button{position:absolute;bottom:20px}.mdl-textfield--align-right{text-align:right}.mdl-textfield--full-width{width:100%}.mdl-textfield--expandable{min-width:32px;width:auto;min-height:32px}.mdl-textfield__input{border:none;border-bottom:1px solid rgba(0,0,0,.12);display:block;font-size:16px;font-family:"Helvetica","Arial",sans-serif;margin:0;padding:4px 0;width:100%;background:0 0;text-align:left;color:inherit}.mdl-textfield__input[type="number"]{-moz-appearance:textfield}.mdl-textfield__input[type="number"]::-webkit-inner-spin-button,.mdl-textfield__input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.mdl-textfield.is-focused .mdl-textfield__input{outline:none}.mdl-textfield.is-invalid .mdl-textfield__input{border-color:#d50000;box-shadow:none}fieldset[disabled] .mdl-textfield .mdl-textfield__input,.mdl-textfield.is-disabled .mdl-textfield__input{background-color:transparent;border-bottom:1px dotted rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-textfield textarea.mdl-textfield__input{display:block}.mdl-textfield__label{bottom:0;color:rgba(0,0,0,.26);font-size:16px;left:0;right:0;pointer-events:none;position:absolute;display:block;top:24px;width:100%;overflow:hidden;white-space:nowrap;text-align:left}.mdl-textfield.is-dirty .mdl-textfield__label,.mdl-textfield.has-placeholder .mdl-textfield__label{visibility:hidden}.mdl-textfield--floating-label .mdl-textfield__label{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{transition:none}fieldset[disabled] .mdl-textfield .mdl-textfield__label,.mdl-textfield.is-disabled.is-disabled .mdl-textfield__label{color:rgba(0,0,0,.26)}.mdl-textfield--floating-label.is-focused .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{color:#3f51b5;font-size:12px;top:4px;visibility:visible}.mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label{top:-16px}.mdl-textfield--floating-label.is-invalid .mdl-textfield__label{color:#d50000;font-size:12px}.mdl-textfield__label:after{background-color:#3f51b5;bottom:20px;content:'';height:2px;left:45%;position:absolute;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden;width:10px}.mdl-textfield.is-focused .mdl-textfield__label:after{left:0;visibility:visible;width:100%}.mdl-textfield.is-invalid .mdl-textfield__label:after{background-color:#d50000}.mdl-textfield__error{color:#d50000;position:absolute;font-size:12px;margin-top:3px;visibility:hidden;display:block}.mdl-textfield.is-invalid .mdl-textfield__error{visibility:visible}.mdl-textfield__expandable-holder{display:inline-block;position:relative;margin-left:32px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-block;max-width:.1px}.mdl-textfield.is-focused .mdl-textfield__expandable-holder,.mdl-textfield.is-dirty .mdl-textfield__expandable-holder{max-width:600px}.mdl-textfield__expandable-holder .mdl-textfield__label:after{bottom:0}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;will-change:transform;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}.mdl-shadow--2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-shadow--3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-shadow--4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)}.mdl-shadow--6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.2)}.mdl-shadow--8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.mdl-shadow--16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.mdl-shadow--24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-grid{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 auto;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.mdl-grid.mdl-grid--no-spacing{padding:0}.mdl-cell{box-sizing:border-box}.mdl-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.mdl-cell--middle{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.mdl-cell--stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch}.mdl-grid.mdl-grid--no-spacing>.mdl-cell{margin:0}.mdl-cell--order-1{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12{-webkit-order:12;-ms-flex-order:12;order:12}@media (max-width:479px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-cell--hide-phone{display:none!important}.mdl-cell--order-1-phone.mdl-cell--order-1-phone{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-phone.mdl-cell--order-2-phone{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-phone.mdl-cell--order-3-phone{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-phone.mdl-cell--order-4-phone{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-phone.mdl-cell--order-5-phone{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-phone.mdl-cell--order-6-phone{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-phone.mdl-cell--order-7-phone{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-phone.mdl-cell--order-8-phone{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-phone.mdl-cell--order-9-phone{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-phone.mdl-cell--order-10-phone{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-phone.mdl-cell--order-11-phone{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-phone.mdl-cell--order-12-phone{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:25%}.mdl-cell--2-col,.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:50%}.mdl-cell--3-col,.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:75%}.mdl-cell--4-col,.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:100%}.mdl-cell--5-col,.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:100%}.mdl-cell--6-col,.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:100%}.mdl-cell--7-col,.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:100%}.mdl-cell--8-col,.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:25%}.mdl-cell--2-offset,.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:50%}.mdl-cell--3-offset,.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:75%}}@media (min-width:480px) and (max-width:839px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:50%}.mdl-cell--hide-tablet{display:none!important}.mdl-cell--order-1-tablet.mdl-cell--order-1-tablet{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-tablet.mdl-cell--order-2-tablet{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-tablet.mdl-cell--order-3-tablet{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-tablet.mdl-cell--order-4-tablet{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-tablet.mdl-cell--order-5-tablet{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-tablet.mdl-cell--order-6-tablet{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-tablet.mdl-cell--order-7-tablet{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-tablet.mdl-cell--order-8-tablet{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-tablet.mdl-cell--order-9-tablet{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-tablet.mdl-cell--order-10-tablet{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-tablet.mdl-cell--order-11-tablet{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-tablet.mdl-cell--order-12-tablet{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:calc(12.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:12.5%}.mdl-cell--2-col,.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:25%}.mdl-cell--3-col,.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:calc(37.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:37.5%}.mdl-cell--4-col,.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:50%}.mdl-cell--5-col,.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:calc(62.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:62.5%}.mdl-cell--6-col,.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:75%}.mdl-cell--7-col,.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:calc(87.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:87.5%}.mdl-cell--8-col,.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:calc(12.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:12.5%}.mdl-cell--2-offset,.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:25%}.mdl-cell--3-offset,.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:calc(37.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:37.5%}.mdl-cell--4-offset,.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:50%}.mdl-cell--5-offset,.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:calc(62.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:62.5%}.mdl-cell--6-offset,.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:75%}.mdl-cell--7-offset,.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:calc(87.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:87.5%}}@media (min-width:840px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:33.3333333333%}.mdl-cell--hide-desktop{display:none!important}.mdl-cell--order-1-desktop.mdl-cell--order-1-desktop{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-desktop.mdl-cell--order-2-desktop{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-desktop.mdl-cell--order-3-desktop{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-desktop.mdl-cell--order-4-desktop{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-desktop.mdl-cell--order-5-desktop{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-desktop.mdl-cell--order-6-desktop{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-desktop.mdl-cell--order-7-desktop{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-desktop.mdl-cell--order-8-desktop{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-desktop.mdl-cell--order-9-desktop{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-desktop.mdl-cell--order-10-desktop{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-desktop.mdl-cell--order-11-desktop{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-desktop.mdl-cell--order-12-desktop{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:calc(8.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:8.3333333333%}.mdl-cell--2-col,.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:calc(16.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:16.6666666667%}.mdl-cell--3-col,.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:25%}.mdl-cell--4-col,.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:33.3333333333%}.mdl-cell--5-col,.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:calc(41.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:41.6666666667%}.mdl-cell--6-col,.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:50%}.mdl-cell--7-col,.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:calc(58.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:58.3333333333%}.mdl-cell--8-col,.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:calc(66.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:66.6666666667%}.mdl-cell--9-col,.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:75%}.mdl-cell--10-col,.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:calc(83.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:83.3333333333%}.mdl-cell--11-col,.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:calc(91.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:91.6666666667%}.mdl-cell--12-col,.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:calc(8.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:8.3333333333%}.mdl-cell--2-offset,.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:calc(16.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:16.6666666667%}.mdl-cell--3-offset,.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:25%}.mdl-cell--4-offset,.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:calc(33.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:33.3333333333%}.mdl-cell--5-offset,.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:calc(41.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:41.6666666667%}.mdl-cell--6-offset,.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:50%}.mdl-cell--7-offset,.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:calc(58.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:58.3333333333%}.mdl-cell--8-offset,.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:calc(66.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:66.6666666667%}.mdl-cell--9-offset,.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:75%}.mdl-cell--10-offset,.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:calc(83.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:83.3333333333%}.mdl-cell--11-offset,.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:calc(91.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:91.6666666667%}} -/*# sourceMappingURL=material.min.css.map */ diff --git a/etc/cli.angular.io/theme.css b/etc/cli.angular.io/theme.css deleted file mode 100644 index b6a336e98b0c..000000000000 --- a/etc/cli.angular.io/theme.css +++ /dev/null @@ -1 +0,0 @@ -.console{width:360px;max-width:92vw;margin-left:15px;margin-right:40px;text-align:left;border-radius:5px;margin-bottom:10px}@media (max-width:830px){.console{margin-right:auto;margin-left:auto}}.console__head{overflow:hidden;background-color:#d5d5d5;padding:8px 15px;border-top-left-radius:5px;border-top-right-radius:5px}.console__dot{float:left;width:12px;height:12px;border-radius:50%;margin-right:7px;box-shadow:0 1px 1px 0 rgba(0,0,0,.2)}.console__dot--red{background-color:#ff6057}.console__dot--yellow{background-color:#ffc22e}.console__dot--green{background-color:#28ca40}.console__body{background-color:#1e1e1e;padding:30px 17px 20px;border-bottom-left-radius:5px;border-bottom-right-radius:5px}.console__prompt{display:block;margin-bottom:15px;font-family:"Source Code Pro",monospace;font-size:15px}.console__prompt::before{content:">";padding-right:15px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-base{height:100vh} diff --git a/etc/rules/README.md b/etc/rules/README.md deleted file mode 100644 index 616923160a2b..000000000000 --- a/etc/rules/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# TsLint Rules - -This folder contains custom TsLint rules specific to this repository. diff --git a/etc/rules/Rule.ts b/etc/rules/Rule.ts deleted file mode 100644 index 43a7df63b777..000000000000 --- a/etc/rules/Rule.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 * as Lint from 'tslint'; -import * as ts from 'typescript'; - - -// An empty rule so that tslint does not error on rules '//' (which are comments). -export class Rule extends Lint.Rules.AbstractRule { - public static metadata: Lint.IRuleMetadata = { - ruleName: '//', - type: 'typescript', - description: ``, - rationale: '', - options: null, - optionsDescription: `Not configurable.`, - typescriptOnly: false, - }; - - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return []; - } -} diff --git a/etc/rules/defocusRule.ts b/etc/rules/defocusRule.ts deleted file mode 100644 index 0634596fa24f..000000000000 --- a/etc/rules/defocusRule.ts +++ /dev/null @@ -1,55 +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 - */ - -/* - * Taken from https://github.com/Sergiioo/tslint-defocus - * Copyright (c) 2016 Sergio Annecchiarico - * MIT - https://github.com/Sergiioo/tslint-defocus/blob/master/LICENSE - */ - -import * as Lint from 'tslint'; -import * as ts from 'typescript'; - -export class Rule extends Lint.Rules.AbstractRule { - - public static metadata: Lint.IRuleMetadata = { - ruleName: 'defocus', - description: "Bans the use of `fdescribe` and 'fit' Jasmine functions.", - rationale: 'It is all too easy to mistakenly commit a focussed Jasmine test suite or spec.', - options: null, - optionsDescription: 'Not configurable.', - type: 'functionality', - typescriptOnly: false, - }; - - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithFunction(sourceFile, walk); - } -} - -function walk(ctx: Lint.WalkContext) { - return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void { - if (node.kind === ts.SyntaxKind.CallExpression) { - const expression = (node as ts.CallExpression).expression; - const functionName = expression.getText(); - bannedFunctions.forEach((banned) => { - if (banned === functionName) { - ctx.addFailureAtNode(expression, failureMessage(functionName)); - } - }); - } - - return ts.forEachChild(node, cb); - }); -} - -const bannedFunctions: ReadonlyArray = ['fdescribe', 'fit']; - -const failureMessage = (functionName: string) => { - return `Calls to '${functionName}' are not allowed.`; -}; diff --git a/etc/rules/importGroupsRule.ts b/etc/rules/importGroupsRule.ts deleted file mode 100644 index c53fcba8c7d2..000000000000 --- a/etc/rules/importGroupsRule.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as Lint from 'tslint'; -import * as ts from 'typescript'; - - -export class Rule extends Lint.Rules.AbstractRule { - public static metadata: Lint.IRuleMetadata = { - ruleName: 'import-groups', - type: 'style', - description: `Ensure imports are grouped.`, - rationale: `Imports can be grouped or not depending on a project. A group is a sequence of - import statements separated by blank lines.`, - options: null, - optionsDescription: `Not configurable.`, - typescriptOnly: false, - }; - - public static FAILURE_STRING = 'You need to keep imports grouped.'; - - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); - } -} - - -class Walker extends Lint.RuleWalker { - walk(sourceFile: ts.SourceFile) { - super.walk(sourceFile); - - const statements = sourceFile.statements; - const imports = statements.filter(s => s.kind == ts.SyntaxKind.ImportDeclaration); - const nonImports = statements.filter(s => s.kind != ts.SyntaxKind.ImportDeclaration); - - for (let i = 1; i < imports.length; i++) { - const node = imports[i]; - const previous = imports[i - 1]; - - if (previous && previous.kind == ts.SyntaxKind.ImportDeclaration) { - const nodeLine = sourceFile.getLineAndCharacterOfPosition(node.getStart()); - const previousLine = sourceFile.getLineAndCharacterOfPosition(previous.getEnd()); - - if (previousLine.line < nodeLine.line - 1) { - if (nonImports.some(s => s.getStart() > previous.getEnd() - && s.getStart() < node.getStart())) { - // Ignore imports with non-imports statements in between. - continue; - } - - this.addFailureAt( - node.getStart(), - node.getWidth(), - Rule.FAILURE_STRING, - Lint.Replacement.deleteFromTo(previous.getEnd() + 1, node.getStart()), - ); - } - } - } - } -} diff --git a/etc/rules/noGlobalTslintDisableRule.ts b/etc/rules/noGlobalTslintDisableRule.ts deleted file mode 100644 index fea185ec7928..000000000000 --- a/etc/rules/noGlobalTslintDisableRule.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as path from 'path'; -import * as Lint from 'tslint'; -import * as ts from 'typescript'; - - -export class Rule extends Lint.Rules.AbstractRule { - public static metadata: Lint.IRuleMetadata = { - ruleName: 'no-global-tslint-disable', - type: 'style', - description: `Ensure global tslint disable are only used for unit tests.`, - rationale: `Some projects want to disallow tslint disable and only use per-line ones.`, - options: null, - optionsDescription: `Not configurable.`, - typescriptOnly: false, - }; - - public static FAILURE_STRING = 'tslint:disable is not allowed in this context.'; - - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); - } -} - - -class Walker extends Lint.RuleWalker { - private _findComments(node: ts.Node): ts.CommentRange[] { - return ([] as ts.CommentRange[]).concat( - ts.getLeadingCommentRanges(node.getFullText(), 0) || [], - ts.getTrailingCommentRanges(node.getFullText(), 0) || [], - node.getChildren().reduce((acc, n) => { - return acc.concat(this._findComments(n)); - }, [] as ts.CommentRange[]), - ); - } - - walk(sourceFile: ts.SourceFile) { - super.walk(sourceFile); - - // Ignore spec files. - if (sourceFile.fileName.match(/_spec(_large)?.ts$/)) { - return; - } - // Ignore benchmark files. - if (sourceFile.fileName.match(/_benchmark.ts$/)) { - return; - } - - // TODO(filipesilva): remove this once the files are cleaned up. - // Ignore Angular CLI files files. - if (sourceFile.fileName.includes('/angular-cli-files/')) { - return; - } - - const scriptsPath = path.join(process.cwd(), 'scripts').replace(/\\/g, '/'); - if (sourceFile.fileName.startsWith(scriptsPath)) { - return; - } - - // Find all comment nodes. - const ranges = this._findComments(sourceFile); - ranges.forEach(range => { - const text = sourceFile.getFullText().substring(range.pos, range.end); - let i = text.indexOf('tslint:disable:'); - - while (i != -1) { - this.addFailureAt(range.pos + i + 1, range.pos + i + 15, Rule.FAILURE_STRING); - i = text.indexOf('tslint:disable:', i + 1); - } - }); - } -} diff --git a/etc/rules/singleEofLineRule.ts b/etc/rules/singleEofLineRule.ts deleted file mode 100644 index b2882e43ffb9..000000000000 --- a/etc/rules/singleEofLineRule.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as Lint from 'tslint'; -import * as ts from 'typescript'; - - -export class Rule extends Lint.Rules.AbstractRule { - public static metadata: Lint.IRuleMetadata = { - ruleName: 'single-eof-line', - type: 'style', - description: `Ensure the file ends with a single new line.`, - rationale: `This is similar to eofline, but ensure an exact count instead of just any new - line.`, - options: null, - optionsDescription: `Two integers indicating minimum and maximum number of new lines.`, - typescriptOnly: false, - }; - - public static FAILURE_STRING = 'You need to have a single blank line at end of file.'; - - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - const length = sourceFile.text.length; - if (length === 0) { - // Allow empty files. - return []; - } - - const matchEof = /\r?\n((\r?\n)*)$/.exec(sourceFile.text); - if (!matchEof) { - const lines = sourceFile.getLineStarts(); - const fix = Lint.Replacement.appendText( - length, - sourceFile.text[lines[1] - 2] === '\r' ? '\r\n' : '\n', - ); - - return [ - new Lint.RuleFailure(sourceFile, length, length, Rule.FAILURE_STRING, this.ruleName, fix), - ]; - } else if (matchEof[1]) { - const lines = sourceFile.getLineStarts(); - const fix = Lint.Replacement.replaceFromTo( - matchEof.index, - length, - sourceFile.text[lines[1] - 2] === '\r' ? '\r\n' : '\n', - ); - - return [ - new Lint.RuleFailure(sourceFile, length, length, Rule.FAILURE_STRING, this.ruleName, fix), - ]; - } - - return []; - } -} diff --git a/etc/rules/tsconfig.json b/etc/rules/tsconfig.json deleted file mode 100644 index dce8cbca6f6f..000000000000 --- a/etc/rules/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - // This tsconfig is to help only build the tslint rules instead of the whole project. Makes - // things faster. - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/etc/rules", - "baseUrl": "" - }, - "exclude": [ - ] -} diff --git a/goldens/BUILD.bazel b/goldens/BUILD.bazel new file mode 100644 index 000000000000..6dbbdd28f25b --- /dev/null +++ b/goldens/BUILD.bazel @@ -0,0 +1,10 @@ +load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin") + +package(default_visibility = ["//visibility:public"]) + +copy_to_bin( + name = "public-api", + srcs = glob([ + "public-api/**/*.md", + ]), +) diff --git a/goldens/circular-deps/packages.json b/goldens/circular-deps/packages.json new file mode 100644 index 000000000000..96a53f7a1040 --- /dev/null +++ b/goldens/circular-deps/packages.json @@ -0,0 +1,42 @@ +[ + [ + "packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts", + "packages/angular_devkit/build_angular/src/builders/dev-server/options.ts" + ], + [ + "packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts", + "packages/angular/build/src/tools/esbuild/bundler-context.ts", + "packages/angular/build/src/tools/esbuild/utils.ts", + "packages/angular/build/src/tools/esbuild/bundler-execution-result.ts" + ], + [ + "packages/angular/build/src/tools/esbuild/bundler-context.ts", + "packages/angular/build/src/tools/esbuild/utils.ts" + ], + [ + "packages/angular/build/src/tools/esbuild/bundler-context.ts", + "packages/angular/build/src/tools/esbuild/utils.ts", + "packages/angular/build/src/tools/esbuild/bundler-execution-result.ts" + ], + [ + "packages/angular/build/src/tools/esbuild/bundler-context.ts", + "packages/angular/build/src/tools/esbuild/utils.ts", + "packages/angular/build/src/utils/server-rendering/manifest.ts" + ], + [ + "packages/angular/build/src/tools/esbuild/bundler-execution-result.ts", + "packages/angular/build/src/tools/esbuild/utils.ts" + ], + [ + "packages/angular/build/src/tools/esbuild/utils.ts", + "packages/angular/build/src/utils/server-rendering/manifest.ts" + ], + [ + "packages/angular/cli/src/analytics/analytics-collector.ts", + "packages/angular/cli/src/command-builder/command-module.ts" + ], + [ + "packages/angular/cli/src/analytics/analytics.ts", + "packages/angular/cli/src/command-builder/command-module.ts" + ] +] diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md new file mode 100644 index 000000000000..6c0c2f19145b --- /dev/null +++ b/goldens/public-api/angular/build/index.api.md @@ -0,0 +1,176 @@ +## API Report File for "@angular/build" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BuilderContext } from '@angular-devkit/architect'; +import { BuilderOutput } from '@angular-devkit/architect'; +import type http from 'node:http'; +import { OutputFile } from 'esbuild'; +import type { Plugin as Plugin_2 } from 'esbuild'; + +// @public (undocumented) +export interface ApplicationBuilderExtensions { + // (undocumented) + codePlugins?: Plugin_2[]; + // (undocumented) + indexHtmlTransformer?: IndexHtmlTransform; +} + +// @public +export type ApplicationBuilderOptions = { + allowedCommonJsDependencies?: string[]; + aot?: boolean; + appShell?: boolean; + assets?: AssetPattern[]; + baseHref?: string; + browser: string; + budgets?: Budget[]; + clearScreen?: boolean; + conditions?: string[]; + crossOrigin?: CrossOrigin; + define?: { + [key: string]: string; + }; + deleteOutputPath?: boolean; + deployUrl?: string; + externalDependencies?: string[]; + extractLicenses?: boolean; + fileReplacements?: FileReplacement[]; + i18nDuplicateTranslation?: I18NTranslation; + i18nMissingTranslation?: I18NTranslation; + index?: IndexUnion; + inlineStyleLanguage?: InlineStyleLanguage; + loader?: { + [key: string]: any; + }; + localize?: Localize; + namedChunks?: boolean; + optimization?: OptimizationUnion; + outputHashing?: OutputHashing; + outputMode?: OutputMode; + outputPath?: OutputPathUnion; + poll?: number; + polyfills?: string[]; + prerender?: PrerenderUnion; + preserveSymlinks?: boolean; + progress?: boolean; + scripts?: ScriptElement[]; + security?: Security; + server?: Serv; + serviceWorker?: Serv; + sourceMap?: SourceMapUnion; + ssr?: SsrUnion; + statsJson?: boolean; + stylePreprocessorOptions?: StylePreprocessorOptions; + styles?: StyleElement[]; + subresourceIntegrity?: boolean; + tsConfig: string; + verbose?: boolean; + watch?: boolean; + webWorkerTsConfig?: string; +}; + +// @public +export function buildApplication(options: ApplicationBuilderOptions, context: BuilderContext, extensions?: ApplicationBuilderExtensions): AsyncIterable; + +// @public (undocumented) +export interface BuildOutputAsset { + // (undocumented) + destination: string; + // (undocumented) + source: string; +} + +// @public (undocumented) +export interface BuildOutputFile extends OutputFile { + // (undocumented) + clone: () => BuildOutputFile; + // (undocumented) + readonly size: number; + // (undocumented) + type: BuildOutputFileType; +} + +// @public (undocumented) +export enum BuildOutputFileType { + // (undocumented) + Browser = 0, + // (undocumented) + Media = 1, + // (undocumented) + Root = 4, + // (undocumented) + ServerApplication = 2, + // (undocumented) + ServerRoot = 3 +} + +// @public +export type DevServerBuilderOptions = { + allowedHosts?: AllowedHosts; + buildTarget: string; + headers?: { + [key: string]: string; + }; + hmr?: boolean; + host?: string; + inspect?: Inspect; + liveReload?: boolean; + open?: boolean; + poll?: number; + port?: number; + prebundle?: PrebundleUnion; + proxyConfig?: string; + servePath?: string; + ssl?: boolean; + sslCert?: string; + sslKey?: string; + verbose?: boolean; + watch?: boolean; +}; + +// @public +export interface DevServerBuilderOutput extends BuilderOutput { + // (undocumented) + address?: string; + // (undocumented) + baseUrl: string; + // (undocumented) + port?: number; +} + +// @public +export function executeDevServerBuilder(options: DevServerBuilderOptions, context: BuilderContext, extensions?: { + buildPlugins?: Plugin_2[]; + middleware?: ((req: http.IncomingMessage, res: http.ServerResponse, next: (err?: unknown) => void) => void)[]; + indexHtmlTransformer?: IndexHtmlTransform; +}): AsyncIterable; + +// @public +export function executeExtractI18nBuilder(options: ExtractI18nBuilderOptions, context: BuilderContext, extensions?: ApplicationBuilderExtensions): Promise; + +// @public +export function executeNgPackagrBuilder(options: NgPackagrBuilderOptions, context: BuilderContext): AsyncIterableIterator; + +// @public +export type ExtractI18nBuilderOptions = { + buildTarget?: string; + format?: Format; + outFile?: string; + outputPath?: string; + progress?: boolean; +}; + +// @public +export type NgPackagrBuilderOptions = { + poll?: number; + project?: string; + tsConfig?: string; + watch?: boolean; +}; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular/ssr/index.api.md b/goldens/public-api/angular/ssr/index.api.md new file mode 100644 index 000000000000..81764fcc1f62 --- /dev/null +++ b/goldens/public-api/angular/ssr/index.api.md @@ -0,0 +1,82 @@ +## API Report File for "@angular/ssr" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { DefaultExport } from '@angular/router'; +import { EnvironmentProviders } from '@angular/core'; +import { Provider } from '@angular/core'; +import { Type } from '@angular/core'; + +// @public +export class AngularAppEngine { + handle(request: Request, requestContext?: unknown): Promise; + static ɵallowStaticRouteRender: boolean; + static ɵhooks: Hooks; +} + +// @public +export function createRequestHandler(handler: RequestHandlerFunction): RequestHandlerFunction; + +// @public +export enum PrerenderFallback { + Client = 1, + None = 2, + Server = 0 +} + +// @public +export function provideServerRendering(...features: ServerRenderingFeature[]): EnvironmentProviders; + +// @public +export enum RenderMode { + Client = 1, + Prerender = 2, + Server = 0 +} + +// @public +export type RequestHandlerFunction = (request: Request) => Promise | null | Response; + +// @public +export type ServerRoute = ServerRouteClient | ServerRoutePrerender | ServerRoutePrerenderWithParams | ServerRouteServer; + +// @public +export interface ServerRouteClient extends ServerRouteCommon { + renderMode: RenderMode.Client; +} + +// @public +export interface ServerRouteCommon { + headers?: Record; + path: string; + status?: number; +} + +// @public +export interface ServerRoutePrerender extends Omit { + fallback?: never; + renderMode: RenderMode.Prerender; +} + +// @public +export interface ServerRoutePrerenderWithParams extends Omit { + fallback?: PrerenderFallback; + getPrerenderParams: () => Promise[]>; +} + +// @public +export interface ServerRouteServer extends ServerRouteCommon { + renderMode: RenderMode.Server; +} + +// @public +export function withAppShell(component: Type | (() => Promise | DefaultExport>>)): ServerRenderingFeature; + +// @public +export function withRoutes(routes: ServerRoute[]): ServerRenderingFeature; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular/ssr/node/index.api.md b/goldens/public-api/angular/ssr/node/index.api.md new file mode 100644 index 000000000000..0f30fa7e99c5 --- /dev/null +++ b/goldens/public-api/angular/ssr/node/index.api.md @@ -0,0 +1,65 @@ +## API Report File for "@angular/ssr_node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApplicationRef } from '@angular/core'; +import { Http2ServerRequest } from 'node:http2'; +import { Http2ServerResponse } from 'node:http2'; +import { IncomingMessage } from 'node:http'; +import { ServerResponse } from 'node:http'; +import { StaticProvider } from '@angular/core'; +import { Type } from '@angular/core'; + +// @public +export class AngularNodeAppEngine { + constructor(); + handle(request: IncomingMessage | Http2ServerRequest, requestContext?: unknown): Promise; +} + +// @public +export class CommonEngine { + constructor(options?: CommonEngineOptions | undefined); + render(opts: CommonEngineRenderOptions): Promise; +} + +// @public (undocumented) +export interface CommonEngineOptions { + bootstrap?: Type<{}> | (() => Promise); + enablePerformanceProfiler?: boolean; + providers?: StaticProvider[]; +} + +// @public (undocumented) +export interface CommonEngineRenderOptions { + bootstrap?: Type<{}> | (() => Promise); + // (undocumented) + document?: string; + // (undocumented) + documentFilePath?: string; + inlineCriticalCss?: boolean; + providers?: StaticProvider[]; + publicPath?: string; + // (undocumented) + url?: string; +} + +// @public +export function createNodeRequestHandler(handler: T): T; + +// @public +export function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage | Http2ServerRequest): Request; + +// @public +export function isMainModule(url: string): boolean; + +// @public +export type NodeRequestHandlerFunction = (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => Promise | void; + +// @public +export function writeResponseToNodeResponse(source: Response, destination: ServerResponse | Http2ServerResponse): Promise; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular_devkit/architect/index.api.md b/goldens/public-api/angular_devkit/architect/index.api.md new file mode 100644 index 000000000000..0ae8751719b5 --- /dev/null +++ b/goldens/public-api/angular_devkit/architect/index.api.md @@ -0,0 +1,553 @@ +## API Report File for "@angular-devkit/architect" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BaseException } from '@angular-devkit/core'; +import { json } from '@angular-devkit/core'; +import { JsonObject } from '@angular-devkit/core'; +import { JsonValue } from '@angular-devkit/core'; +import { logging } from '@angular-devkit/core'; +import { Observable } from 'rxjs'; +import { ObservableInput } from 'rxjs'; +import { Observer } from 'rxjs'; +import { schema } from '@angular-devkit/core'; + +// @public (undocumented) +export class Architect { + constructor(_host: ArchitectHost, registry?: json.schema.SchemaRegistry, additionalJobRegistry?: Registry); + // (undocumented) + has(name: JobName): Observable; + // (undocumented) + scheduleBuilder(name: string, options: json.JsonObject, scheduleOptions?: ScheduleOptions): Promise; + // (undocumented) + scheduleTarget(target: Target, overrides?: json.JsonObject, scheduleOptions?: ScheduleOptions): Promise; +} + +// @public +export interface Builder { + // (undocumented) + [BuilderSymbol]: true; + // (undocumented) + [BuilderVersionSymbol]: string; + // (undocumented) + __OptionT: OptionT; + // (undocumented) + handler: JobHandler; +} + +// @public +export interface BuilderContext { + addTeardown(teardown: () => Promise | void): void; + builder: BuilderInfo; + currentDirectory: string; + getBuilderNameForTarget(target: Target): Promise; + // (undocumented) + getProjectMetadata(projectName: string): Promise; + // (undocumented) + getProjectMetadata(target: Target): Promise; + getTargetOptions(target: Target): Promise; + id: number; + logger: logging.LoggerApi; + reportProgress(current: number, total?: number, status?: string): void; + reportRunning(): void; + reportStatus(status: string): void; + scheduleBuilder(builderName: string, options?: json.JsonObject, scheduleOptions?: ScheduleOptions_2): Promise; + scheduleTarget(target: Target, overrides?: json.JsonObject, scheduleOptions?: ScheduleOptions_2): Promise; + target?: Target; + validateOptions(options: json.JsonObject, builderName: string): Promise; + workspaceRoot: string; +} + +// @public +export interface BuilderHandlerFn { + (input: A, context: BuilderContext): BuilderOutputLike; +} + +// @public +export type BuilderInfo = json.JsonObject & { + builderName: string; + description: string; + optionSchema: json.schema.JsonSchema; +}; + +// @public +export type BuilderInput = json.JsonObject & Schema; + +// @public (undocumented) +export type BuilderOutput = json.JsonObject & Schema_2; + +// @public +export type BuilderOutputLike = ObservableInput | BuilderOutput; + +// @public (undocumented) +export type BuilderProgress = json.JsonObject & Schema_3 & TypedBuilderProgress; + +// @public +export type BuilderProgressReport = BuilderProgress & { + target?: Target; + builder: BuilderInfo; +}; + +// @public (undocumented) +export enum BuilderProgressState { + // (undocumented) + Error = "error", + // (undocumented) + Running = "running", + // (undocumented) + Stopped = "stopped", + // (undocumented) + Waiting = "waiting" +} + +// @public (undocumented) +export type BuilderRegistry = Registry; + +// @public +export interface BuilderRun { + id: number; + info: BuilderInfo; + lastOutput: Promise; + output: Observable; + progress: Observable; + result: Promise; + stop(): Promise; +} + +// @public (undocumented) +class ChannelAlreadyExistException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export function createBuilder(fn: BuilderHandlerFn): Builder; + +// @public +function createDispatcher(options?: Partial>): JobDispatcher; + +// @public +function createJobFactory(loader: () => Promise>, options?: Partial): JobHandler; + +// @public +function createJobHandler(fn: SimpleJobHandlerFn, options?: Partial): JobHandler; + +// @public +function createLoggerJob(job: JobHandler, logger: logging.LoggerApi): JobHandler; + +// @public +class FallbackRegistry implements Registry { + constructor(_fallbacks?: Registry[]); + // (undocumented) + addFallback(registry: Registry): void; + // (undocumented) + protected _fallbacks: Registry[]; + // (undocumented) + get(name: JobName): Observable | null>; +} + +// @public (undocumented) +export function fromAsyncIterable(iterable: AsyncIterable): Observable; + +// @public (undocumented) +export function isBuilderOutput(obj: any): obj is BuilderOutput; + +// @public (undocumented) +function isJobHandler(value: unknown): value is JobHandler; + +// @public +interface Job { + readonly argument: ArgumentT; + readonly description: Observable; + getChannel(name: string, schema?: schema.JsonSchema): Observable; + readonly inboundBus: Observer>; + readonly input: Observer; + readonly outboundBus: Observable>; + readonly output: Observable; + ping(): Observable; + readonly state: JobState; + stop(): void; +} + +// @public (undocumented) +class JobArgumentSchemaValidationError extends schema.SchemaValidationException { + constructor(errors?: schema.SchemaValidatorError[]); +} + +// @public +interface JobDescription extends JsonObject { + // (undocumented) + readonly argument: DeepReadonly; + // (undocumented) + readonly input: DeepReadonly; + // (undocumented) + readonly name: JobName; + // (undocumented) + readonly output: DeepReadonly; +} + +// @public +interface JobDispatcher extends JobHandler { + addConditionalJob(predicate: (args: A) => boolean, name: string): void; + setDefaultJob(name: JobName | null | JobHandler): void; +} + +// @public (undocumented) +class JobDoesNotExistException extends BaseException { + constructor(name: JobName); +} + +// @public +interface JobHandler { + // (undocumented) + (argument: ArgT, context: JobHandlerContext): Observable>; + // (undocumented) + jobDescription: Partial; +} + +// @public +interface JobHandlerContext { + // (undocumented) + readonly dependencies: Job[]; + // (undocumented) + readonly description: JobDescription; + // (undocumented) + readonly inboundBus: Observable>; + // (undocumented) + readonly scheduler: Scheduler; +} + +// @public (undocumented) +type JobInboundMessage = JobInboundMessagePing | JobInboundMessageStop | JobInboundMessageInput; + +// @public +interface JobInboundMessageBase extends JsonObject { + readonly kind: JobInboundMessageKind; +} + +// @public +interface JobInboundMessageInput extends JobInboundMessageBase { + // (undocumented) + readonly kind: JobInboundMessageKind.Input; + readonly value: InputT; +} + +// @public +enum JobInboundMessageKind { + // (undocumented) + Input = "in", + // (undocumented) + Ping = "ip", + // (undocumented) + Stop = "is" +} + +// @public +interface JobInboundMessagePing extends JobInboundMessageBase { + readonly id: number; + // (undocumented) + readonly kind: JobInboundMessageKind.Ping; +} + +// @public (undocumented) +class JobInboundMessageSchemaValidationError extends schema.SchemaValidationException { + constructor(errors?: schema.SchemaValidatorError[]); +} + +// @public +interface JobInboundMessageStop extends JobInboundMessageBase { + // (undocumented) + readonly kind: JobInboundMessageKind.Stop; +} + +// @public +type JobName = string; + +// @public (undocumented) +class JobNameAlreadyRegisteredException extends BaseException { + constructor(name: JobName); +} + +// @public +type JobOutboundMessage = JobOutboundMessageOnReady | JobOutboundMessageStart | JobOutboundMessageOutput | JobOutboundMessageChannelCreate | JobOutboundMessageChannelMessage | JobOutboundMessageChannelError | JobOutboundMessageChannelComplete | JobOutboundMessageEnd | JobOutboundMessagePong; + +// @public +interface JobOutboundMessageBase { + readonly description: JobDescription; + readonly kind: JobOutboundMessageKind; +} + +// @public +interface JobOutboundMessageChannelBase extends JobOutboundMessageBase { + readonly name: string; +} + +// @public +interface JobOutboundMessageChannelComplete extends JobOutboundMessageChannelBase { + // (undocumented) + readonly kind: JobOutboundMessageKind.ChannelComplete; +} + +// @public +interface JobOutboundMessageChannelCreate extends JobOutboundMessageChannelBase { + // (undocumented) + readonly kind: JobOutboundMessageKind.ChannelCreate; +} + +// @public +interface JobOutboundMessageChannelError extends JobOutboundMessageChannelBase { + readonly error: JsonValue; + // (undocumented) + readonly kind: JobOutboundMessageKind.ChannelError; +} + +// @public +interface JobOutboundMessageChannelMessage extends JobOutboundMessageChannelBase { + // (undocumented) + readonly kind: JobOutboundMessageKind.ChannelMessage; + readonly message: JsonValue; +} + +// @public +interface JobOutboundMessageEnd extends JobOutboundMessageBase { + // (undocumented) + readonly kind: JobOutboundMessageKind.End; +} + +// @public +enum JobOutboundMessageKind { + // (undocumented) + ChannelComplete = "cc", + // (undocumented) + ChannelCreate = "cn", + // (undocumented) + ChannelError = "ce", + // (undocumented) + ChannelMessage = "cm", + // (undocumented) + End = "e", + // (undocumented) + OnReady = "c", + // (undocumented) + Output = "o", + // (undocumented) + Pong = "p", + // (undocumented) + Start = "s" +} + +// @public +interface JobOutboundMessageOnReady extends JobOutboundMessageBase { + // (undocumented) + readonly kind: JobOutboundMessageKind.OnReady; +} + +// @public +interface JobOutboundMessageOutput extends JobOutboundMessageBase { + // (undocumented) + readonly kind: JobOutboundMessageKind.Output; + readonly value: OutputT; +} + +// @public +interface JobOutboundMessagePong extends JobOutboundMessageBase { + readonly id: number; + // (undocumented) + readonly kind: JobOutboundMessageKind.Pong; +} + +// @public +interface JobOutboundMessageStart extends JobOutboundMessageBase { + // (undocumented) + readonly kind: JobOutboundMessageKind.Start; +} + +// @public (undocumented) +class JobOutputSchemaValidationError extends schema.SchemaValidationException { + constructor(errors?: schema.SchemaValidatorError[]); +} + +declare namespace jobs { + export { + strategy, + isJobHandler, + JobName, + JobHandler, + JobHandlerContext, + JobDescription, + JobInboundMessageKind, + JobInboundMessageBase, + JobInboundMessagePing, + JobInboundMessageStop, + JobInboundMessageInput, + JobInboundMessage, + JobOutboundMessageKind, + JobOutboundMessageBase, + JobOutboundMessageOnReady, + JobOutboundMessageStart, + JobOutboundMessageOutput, + JobOutboundMessageChannelBase, + JobOutboundMessageChannelMessage, + JobOutboundMessageChannelError, + JobOutboundMessageChannelCreate, + JobOutboundMessageChannelComplete, + JobOutboundMessageEnd, + JobOutboundMessagePong, + JobOutboundMessage, + JobState, + Job, + ScheduleJobOptions, + Registry, + Scheduler, + createJobHandler, + createJobFactory, + createLoggerJob, + ChannelAlreadyExistException, + SimpleJobHandlerContext, + SimpleJobHandlerFn, + JobNameAlreadyRegisteredException, + JobDoesNotExistException, + createDispatcher, + JobDispatcher, + FallbackRegistry, + RegisterJobOptions, + SimpleJobRegistry, + JobArgumentSchemaValidationError, + JobInboundMessageSchemaValidationError, + JobOutputSchemaValidationError, + SimpleScheduler + } +} +export { jobs } + +// @public +enum JobState { + Ended = "ended", + Errored = "errored", + Queued = "queued", + Ready = "ready", + Started = "started" +} + +// @public (undocumented) +type JobStrategy = (handler: JobHandler, options?: Partial>) => JobHandler; + +// @public +function memoize(replayMessages?: boolean): JobStrategy; + +// @public +interface RegisterJobOptions extends Partial { +} + +// @public (undocumented) +interface Registry { + get(name: JobName): Observable | null>; +} + +// @public +function reuse(replayMessages?: boolean): JobStrategy; + +// @public +interface ScheduleJobOptions { + dependencies?: Job | Job[]; +} + +// @public (undocumented) +export interface ScheduleOptions { + // (undocumented) + logger?: logging.Logger; +} + +// @public +interface Scheduler { + getDescription(name: JobName): Observable; + has(name: JobName): Observable; + pause(): () => void; + schedule(name: JobName, argument: A, options?: ScheduleJobOptions): Job; +} + +// @public +export function scheduleTargetAndForget(context: BuilderContext, target: Target, overrides?: json.JsonObject, scheduleOptions?: ScheduleOptions_2): Observable; + +// @public +function serialize(): JobStrategy; + +// @public +interface SimpleJobHandlerContext extends JobHandlerContext { + // (undocumented) + addTeardown(teardown: () => Promise | void): void; + // (undocumented) + createChannel: (name: string) => Observer; + // (undocumented) + input: Observable; +} + +// @public +type SimpleJobHandlerFn = (input: A, context: SimpleJobHandlerContext) => O | Promise | Observable; + +// @public +class SimpleJobRegistry implements Registry { + // (undocumented) + get(name: JobName): Observable | null>; + getJobNames(): JobName[]; + register(name: JobName, handler: JobHandler, options?: RegisterJobOptions): void; + register(handler: JobHandler, options?: RegisterJobOptions & { + name: string; + }): void; + // (undocumented) + protected _register(name: JobName, handler: JobHandler, options: RegisterJobOptions): void; +} + +// @public +class SimpleScheduler implements Scheduler { + constructor(_jobRegistry: Registry, _schemaRegistry?: schema.SchemaRegistry); + getDescription(name: JobName): Observable; + has(name: JobName): Observable; + // (undocumented) + protected _jobRegistry: Registry; + pause(): () => void; + schedule(name: JobName, argument: A, options?: ScheduleJobOptions): Job; + // (undocumented) + protected _scheduleJob(name: JobName, argument: A, options: ScheduleJobOptions, waitable: Observable): Job; + // (undocumented) + protected _schemaRegistry: schema.SchemaRegistry; +} + +declare namespace strategy { + export { + serialize, + reuse, + memoize, + JobStrategy + } +} + +// @public (undocumented) +export type Target = json.JsonObject & Target_2; + +// @public +export function targetFromTargetString(specifier: string, abbreviatedProjectName?: string, abbreviatedTargetName?: string): Target; + +// @public +export function targetStringFromTarget({ project, target, configuration }: Target): string; + +// @public +export type TypedBuilderProgress = { + state: BuilderProgressState.Stopped; +} | { + state: BuilderProgressState.Error; + error: json.JsonValue; +} | { + state: BuilderProgressState.Waiting; + status?: string; +} | { + state: BuilderProgressState.Running; + status?: string; + current: number; + total?: number; +}; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular_devkit/build_angular/index.api.md b/goldens/public-api/angular_devkit/build_angular/index.api.md new file mode 100644 index 000000000000..9208d1ad56a1 --- /dev/null +++ b/goldens/public-api/angular_devkit/build_angular/index.api.md @@ -0,0 +1,371 @@ +## API Report File for "@angular-devkit/build-angular" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ApplicationBuilderOptions } from '@angular/build'; +import { buildApplication } from '@angular/build'; +import { BuilderContext } from '@angular-devkit/architect'; +import { BuilderOutput } from '@angular-devkit/architect'; +import type { ConfigOptions } from 'karma'; +import { Configuration } from 'webpack'; +import { DevServerBuilderOutput } from '@angular/build'; +import type http from 'node:http'; +import { IndexHtmlTransform } from '@angular/build/private'; +import { Observable } from 'rxjs'; +import type { Plugin as Plugin_2 } from 'esbuild'; +import webpack from 'webpack'; +import { WebpackLoggingCallback } from '@angular-devkit/build-webpack'; + +export { ApplicationBuilderOptions } + +// @public (undocumented) +export type AssetPattern = AssetPatternObject | string; + +// @public (undocumented) +export type AssetPatternObject = { + followSymlinks?: boolean; + glob: string; + ignore?: string[]; + input: string; + output?: string; +}; + +// @public +export type BrowserBuilderOptions = { + allowedCommonJsDependencies?: string[]; + aot?: boolean; + assets?: AssetPattern[]; + baseHref?: string; + budgets?: Budget[]; + buildOptimizer?: boolean; + commonChunk?: boolean; + crossOrigin?: CrossOrigin; + deleteOutputPath?: boolean; + deployUrl?: string; + extractLicenses?: boolean; + fileReplacements?: FileReplacement[]; + i18nDuplicateTranslation?: I18NTranslation; + i18nMissingTranslation?: I18NTranslation; + index: IndexUnion; + inlineStyleLanguage?: InlineStyleLanguage; + localize?: Localize; + main: string; + namedChunks?: boolean; + ngswConfigPath?: string; + optimization?: OptimizationUnion; + outputHashing?: OutputHashing; + outputPath: string; + poll?: number; + polyfills?: Polyfills; + preserveSymlinks?: boolean; + progress?: boolean; + resourcesOutputPath?: string; + scripts?: ScriptElement[]; + serviceWorker?: boolean; + sourceMap?: SourceMapUnion; + statsJson?: boolean; + stylePreprocessorOptions?: StylePreprocessorOptions; + styles?: StyleElement[]; + subresourceIntegrity?: boolean; + tsConfig: string; + vendorChunk?: boolean; + verbose?: boolean; + watch?: boolean; + webWorkerTsConfig?: string; +}; + +// @public +export type BrowserBuilderOutput = BuilderOutput & { + stats: BuildEventStats; + baseOutputPath: string; + outputs: { + locale?: string; + path: string; + baseHref?: string; + }[]; +}; + +// @public (undocumented) +export type Budget = { + baseline?: string; + error?: string; + maximumError?: string; + maximumWarning?: string; + minimumError?: string; + minimumWarning?: string; + name?: string; + type: Type; + warning?: string; +}; + +export { buildApplication } + +// @public +export enum CrossOrigin { + // (undocumented) + Anonymous = "anonymous", + // (undocumented) + None = "none", + // (undocumented) + UseCredentials = "use-credentials" +} + +// @public +export type DevServerBuilderOptions = { + allowedHosts?: string[]; + buildTarget: string; + disableHostCheck?: boolean; + forceEsbuild?: boolean; + headers?: { + [key: string]: string; + }; + hmr?: boolean; + host?: string; + inspect?: Inspect; + liveReload?: boolean; + open?: boolean; + poll?: number; + port?: number; + prebundle?: PrebundleUnion; + proxyConfig?: string; + publicHost?: string; + servePath?: string; + ssl?: boolean; + sslCert?: string; + sslKey?: string; + verbose?: boolean; + watch?: boolean; +}; + +export { DevServerBuilderOutput } + +// @public +export function executeBrowserBuilder(options: BrowserBuilderOptions, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; + logging?: WebpackLoggingCallback; + indexHtml?: IndexHtmlTransform; +}): Observable; + +// @public +export function executeDevServerBuilder(options: DevServerBuilderOptions, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; + logging?: WebpackLoggingCallback; + indexHtml?: IndexHtmlTransform; +}, extensions?: { + buildPlugins?: Plugin_2[]; + middleware?: ((req: http.IncomingMessage, res: http.ServerResponse, next: (err?: unknown) => void) => void)[]; + builderSelector?: (info: BuilderSelectorInfo, logger: BuilderContext['logger']) => string; +}): Observable; + +// @public +export function executeExtractI18nBuilder(options: ExtractI18nBuilderOptions, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; +}): Promise; + +// @public +export function executeKarmaBuilder(options: KarmaBuilderOptions, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; + karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions; +}): Observable; + +// @public +export function executeNgPackagrBuilder(options: NgPackagrBuilderOptions, context: BuilderContext): Observable; + +// @public +export function executeProtractorBuilder(options: ProtractorBuilderOptions, context: BuilderContext): Promise; + +// @public +export function executeServerBuilder(options: ServerBuilderOptions, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; +}): Observable; + +// @public (undocumented) +export function executeSSRDevServerBuilder(options: SSRDevServerBuilderOptions, context: BuilderContext): Observable; + +// @public +export type ExecutionTransformer = (input: T) => T | Promise; + +// @public +export type ExtractI18nBuilderOptions = { + buildTarget?: string; + format?: Format; + outFile?: string; + outputPath?: string; + progress?: boolean; +}; + +// @public (undocumented) +export type FileReplacement = { + replace?: string; + replaceWith?: string; + src?: string; + with?: string; +}; + +// @public +export type KarmaBuilderOptions = { + aot?: boolean; + assets?: AssetPattern_2[]; + browsers?: Browsers; + builderMode?: BuilderMode; + codeCoverage?: boolean; + codeCoverageExclude?: string[]; + exclude?: string[]; + fileReplacements?: FileReplacement_2[]; + include?: string[]; + inlineStyleLanguage?: InlineStyleLanguage_2; + karmaConfig?: string; + main?: string; + poll?: number; + polyfills?: Polyfills_2; + preserveSymlinks?: boolean; + progress?: boolean; + reporters?: string[]; + scripts?: ScriptElement_2[]; + sourceMap?: SourceMapUnion_2; + stylePreprocessorOptions?: StylePreprocessorOptions_2; + styles?: StyleElement_2[]; + tsConfig: string; + watch?: boolean; + webWorkerTsConfig?: string; +}; + +// @public (undocumented) +export type KarmaConfigOptions = ConfigOptions & { + buildWebpack?: unknown; + configFile?: string; +}; + +// @public +export type NgPackagrBuilderOptions = { + poll?: number; + project: string; + tsConfig?: string; + watch?: boolean; +}; + +// @public (undocumented) +export type OptimizationObject = { + fonts?: FontsUnion; + scripts?: boolean; + styles?: StylesUnion; +}; + +// @public +export type OptimizationUnion = boolean | OptimizationObject; + +// @public +export enum OutputHashing { + // (undocumented) + All = "all", + // (undocumented) + Bundles = "bundles", + // (undocumented) + Media = "media", + // (undocumented) + None = "none" +} + +// @public +export type ProtractorBuilderOptions = { + baseUrl?: string; + devServerTarget?: string; + grep?: string; + host?: string; + invertGrep?: boolean; + port?: number; + protractorConfig: string; + specs?: string[]; + suite?: string; + webdriverUpdate?: boolean; +}; + +// @public (undocumented) +export type ServerBuilderOptions = { + assets?: AssetPattern_3[]; + buildOptimizer?: boolean; + deleteOutputPath?: boolean; + deployUrl?: string; + externalDependencies?: string[]; + extractLicenses?: boolean; + fileReplacements?: FileReplacement_3[]; + i18nDuplicateTranslation?: I18NTranslation_2; + i18nMissingTranslation?: I18NTranslation_2; + inlineStyleLanguage?: InlineStyleLanguage_3; + localize?: Localize_2; + main: string; + namedChunks?: boolean; + optimization?: OptimizationUnion_2; + outputHashing?: OutputHashing_2; + outputPath: string; + poll?: number; + preserveSymlinks?: boolean; + progress?: boolean; + resourcesOutputPath?: string; + sourceMap?: SourceMapUnion_3; + statsJson?: boolean; + stylePreprocessorOptions?: StylePreprocessorOptions_3; + tsConfig: string; + vendorChunk?: boolean; + verbose?: boolean; + watch?: boolean; +}; + +// @public +export type ServerBuilderOutput = BuilderOutput & { + baseOutputPath: string; + outputPath: string; + outputs: { + locale?: string; + path: string; + }[]; +}; + +// @public (undocumented) +export type SourceMapObject = { + hidden?: boolean; + scripts?: boolean; + styles?: boolean; + vendor?: boolean; +}; + +// @public +export type SourceMapUnion = boolean | SourceMapObject; + +// @public (undocumented) +export type SSRDevServerBuilderOptions = Schema; + +// @public (undocumented) +export type SSRDevServerBuilderOutput = BuilderOutput & { + baseUrl?: string; + port?: string; +}; + +// @public +export type StylePreprocessorOptions = { + includePaths?: string[]; +}; + +// @public +export enum Type { + // (undocumented) + All = "all", + // (undocumented) + AllScript = "allScript", + // (undocumented) + Any = "any", + // (undocumented) + AnyComponentStyle = "anyComponentStyle", + // (undocumented) + AnyScript = "anyScript", + // (undocumented) + Bundle = "bundle", + // (undocumented) + Initial = "initial" +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular_devkit/build_webpack/index.api.md b/goldens/public-api/angular_devkit/build_webpack/index.api.md new file mode 100644 index 000000000000..8a3fe489f1c1 --- /dev/null +++ b/goldens/public-api/angular_devkit/build_webpack/index.api.md @@ -0,0 +1,79 @@ +## API Report File for "@angular-devkit/build-webpack" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BuilderContext } from '@angular-devkit/architect'; +import { BuilderOutput } from '@angular-devkit/architect'; +import { Observable } from 'rxjs'; +import webpack from 'webpack'; +import WebpackDevServer from 'webpack-dev-server'; + +// @public (undocumented) +export type BuildResult = BuilderOutput & { + emittedFiles?: EmittedFiles[]; + webpackStats?: webpack.StatsCompilation; + outputPath: string; +}; + +// @public (undocumented) +export type DevServerBuildOutput = BuildResult & { + port: number; + family: string; + address: string; +}; + +// @public (undocumented) +export interface EmittedFiles { + // (undocumented) + asset?: boolean; + // (undocumented) + extension: string; + // (undocumented) + file: string; + // (undocumented) + id?: string; + // (undocumented) + initial: boolean; + // (undocumented) + name?: string; +} + +// @public (undocumented) +export function runWebpack(config: webpack.Configuration, context: BuilderContext, options?: { + logging?: WebpackLoggingCallback; + webpackFactory?: WebpackFactory; + shouldProvideStats?: boolean; +}): Observable; + +// @public (undocumented) +export function runWebpackDevServer(config: webpack.Configuration, context: BuilderContext, options?: { + shouldProvideStats?: boolean; + devServerConfig?: WebpackDevServer.Configuration; + logging?: WebpackLoggingCallback; + webpackFactory?: WebpackFactory; + webpackDevServerFactory?: WebpackDevServerFactory; +}): Observable; + +// @public (undocumented) +export type WebpackBuilderSchema = Schema; + +// @public (undocumented) +export type WebpackDevServerFactory = typeof WebpackDevServer; + +// @public (undocumented) +export interface WebpackFactory { + // (undocumented) + (config: webpack.Configuration): Observable | webpack.Compiler; +} + +// @public (undocumented) +export interface WebpackLoggingCallback { + // (undocumented) + (stats: webpack.Stats, config: webpack.Configuration): void; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular_devkit/core/index.api.md b/goldens/public-api/angular_devkit/core/index.api.md new file mode 100644 index 000000000000..61747760e753 --- /dev/null +++ b/goldens/public-api/angular_devkit/core/index.api.md @@ -0,0 +1,1400 @@ +## API Report File for "@angular-devkit/core" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ErrorObject } from 'ajv'; +import { Format } from 'ajv'; +import { Observable } from 'rxjs'; +import { ObservableInput } from 'rxjs'; +import { Operator } from 'rxjs'; +import { PartialObserver } from 'rxjs'; +import { Position } from 'source-map'; +import { Subject } from 'rxjs'; +import { Subscription } from 'rxjs'; +import { ValidateFunction } from 'ajv'; + +// @public (undocumented) +function addUndefinedDefaults(value: JsonValue, _pointer: JsonPointer, schema?: JsonSchema): JsonValue; + +// @public (undocumented) +function addUndefinedObjectDefaults(value: JsonValue, _pointer: JsonPointer, schema?: JsonSchema): JsonValue; + +// @public +class AliasHost extends ResolverHost { + // (undocumented) + get aliases(): Map; + // (undocumented) + protected _aliases: Map; + // (undocumented) + protected _resolve(path: Path): Path; +} + +// @public (undocumented) +export function asPosixPath(path: Path): PosixPath; + +// @public (undocumented) +export function asWindowsPath(path: Path): WindowsPath; + +// @public +export class BaseException extends Error { + constructor(message?: string); +} + +// @public +export function basename(path: Path): PathFragment; + +// @public (undocumented) +function buildJsonPointer(fragments: string[]): JsonPointer; + +// @public +function camelize(str: string): string; + +// @public +function capitalize(str: string): string; + +// @public (undocumented) +export class CircularDependencyFoundException extends BaseException { + constructor(); +} + +// @public +function classify(str: string): string; + +// @public +class CordHost extends SimpleMemoryHost { + constructor(_back: ReadonlyHost); + // (undocumented) + protected _back: ReadonlyHost; + // (undocumented) + get backend(): ReadonlyHost; + // (undocumented) + get capabilities(): HostCapabilities; + clone(): CordHost; + commit(host: Host, force?: boolean): Observable; + create(path: Path, content: FileBuffer): Observable; + // (undocumented) + delete(path: Path): Observable; + // (undocumented) + exists(path: Path): Observable; + // (undocumented) + protected _filesToCreate: Set; + // (undocumented) + protected _filesToDelete: Set; + // (undocumented) + protected _filesToOverwrite: Set; + // (undocumented) + protected _filesToRename: Map; + // (undocumented) + protected _filesToRenameRevert: Map; + // (undocumented) + isDirectory(path: Path): Observable; + // (undocumented) + isFile(path: Path): Observable; + // (undocumented) + list(path: Path): Observable; + // (undocumented) + overwrite(path: Path, content: FileBuffer): Observable; + // (undocumented) + read(path: Path): Observable; + // (undocumented) + records(): CordHostRecord[]; + // (undocumented) + rename(from: Path, to: Path): Observable; + // (undocumented) + stat(path: Path): Observable | null; + // (undocumented) + watch(path: Path, options?: HostWatchOptions): null; + // (undocumented) + willCreate(path: Path): boolean; + // (undocumented) + willDelete(path: Path): boolean; + // (undocumented) + willOverwrite(path: Path): boolean; + // (undocumented) + willRename(path: Path): boolean; + // (undocumented) + willRenameTo(path: Path, to: Path): boolean; + // (undocumented) + write(path: Path, content: FileBuffer): Observable; +} + +// @public (undocumented) +interface CordHostCreate { + // (undocumented) + content: FileBuffer; + // (undocumented) + kind: 'create'; + // (undocumented) + path: Path; +} + +// @public (undocumented) +interface CordHostDelete { + // (undocumented) + kind: 'delete'; + // (undocumented) + path: Path; +} + +// @public (undocumented) +interface CordHostOverwrite { + // (undocumented) + content: FileBuffer; + // (undocumented) + kind: 'overwrite'; + // (undocumented) + path: Path; +} + +// @public (undocumented) +type CordHostRecord = CordHostCreate | CordHostOverwrite | CordHostRename | CordHostDelete; + +// @public (undocumented) +interface CordHostRename { + // (undocumented) + from: Path; + // (undocumented) + kind: 'rename'; + // (undocumented) + to: Path; +} + +// @public (undocumented) +class CoreSchemaRegistry implements SchemaRegistry { + constructor(formats?: SchemaFormat[]); + // (undocumented) + addFormat(format: SchemaFormat): void; + addPostTransform(visitor: JsonVisitor, deps?: JsonVisitor[]): void; + addPreTransform(visitor: JsonVisitor, deps?: JsonVisitor[]): void; + // (undocumented) + addSmartDefaultProvider(source: string, provider: SmartDefaultProvider): void; + compile(schema: JsonSchema): Promise; + // (undocumented) + registerUriHandler(handler: UriHandler): void; + // (undocumented) + protected _resolver(ref: string, validate?: ValidateFunction): { + context?: ValidateFunction; + schema?: JsonObject; + }; + // (undocumented) + usePromptProvider(provider: PromptProvider): void; + // (undocumented) + useXDeprecatedProvider(onUsage: (message: string) => void): void; + ɵflatten(schema: JsonObject): Promise; +} + +// @public (undocumented) +function createSyncHost(handler: SyncHostHandler): Host; + +// @public (undocumented) +function createWorkspaceHost(host: virtualFs.Host): WorkspaceHost; + +// @public +function dasherize(str: string): string; + +// @public +function decamelize(str: string): string; + +// @public +export function deepCopy(value: T): T; + +// @public (undocumented) +type DefinitionCollectionListener = (name: string, newValue: V | undefined, collection: DefinitionCollection) => void; + +// @public (undocumented) +export class DependencyNotFoundException extends BaseException { + constructor(); +} + +// @public +export function dirname(path: Path): Path; + +// @public (undocumented) +class Empty implements ReadonlyHost { + // (undocumented) + readonly capabilities: HostCapabilities; + // (undocumented) + exists(path: Path): Observable; + // (undocumented) + isDirectory(path: Path): Observable; + // (undocumented) + isFile(path: Path): Observable; + // (undocumented) + list(path: Path): Observable; + // (undocumented) + read(path: Path): Observable; + // (undocumented) + stat(path: Path): Observable | null>; +} + +// @public (undocumented) +export function extname(path: Path): string; + +// @public (undocumented) +export class FileAlreadyExistException extends BaseException { + constructor(path: string); +} + +// @public (undocumented) +type FileBuffer = ArrayBuffer; + +// @public (undocumented) +type FileBufferLike = ArrayBufferLike; + +// @public (undocumented) +function fileBufferToString(fileBuffer: FileBuffer): string; + +// @public (undocumented) +export class FileDoesNotExistException extends BaseException { + constructor(path: string); +} + +// @public (undocumented) +export function fragment(path: string): PathFragment; + +// @public (undocumented) +export function getSystemPath(path: Path): string; + +// @public (undocumented) +function getTypesOfSchema(schema: JsonSchema): Set; + +// @public (undocumented) +interface Host extends ReadonlyHost { + // (undocumented) + delete(path: Path): Observable; + // (undocumented) + rename(from: Path, to: Path): Observable; + // (undocumented) + watch(path: Path, options?: HostWatchOptions): Observable | null; + // (undocumented) + write(path: Path, content: FileBufferLike): Observable; +} + +// @public (undocumented) +interface HostCapabilities { + // (undocumented) + synchronous: boolean; +} + +// @public (undocumented) +interface HostWatchEvent { + // (undocumented) + readonly path: Path; + // (undocumented) + readonly time: Date; + // (undocumented) + readonly type: HostWatchEventType; +} + +// @public (undocumented) +enum HostWatchEventType { + // (undocumented) + Changed = 0, + // (undocumented) + Created = 1, + // (undocumented) + Deleted = 2, + // (undocumented) + Renamed = 3 +} + +// @public (undocumented) +interface HostWatchOptions { + // (undocumented) + readonly persistent?: boolean; + // (undocumented) + readonly recursive?: boolean; +} + +// @public (undocumented) +function indentBy(indentations: number): TemplateTag; + +// @public (undocumented) +class IndentLogger extends Logger { + constructor(name: string, parent?: Logger | null, indentation?: string); +} + +// @public (undocumented) +export class InvalidPathException extends BaseException { + constructor(path: string); +} + +// @public +export function isAbsolute(p: Path): boolean; + +// @public (undocumented) +export function isJsonArray(value: JsonValue): value is JsonArray; + +// @public (undocumented) +export function isJsonObject(value: JsonValue): value is JsonObject; + +// @public (undocumented) +function isJsonSchema(value: unknown): value is JsonSchema; + +// @public +export function isPromise(obj: any): obj is Promise; + +// @public +export function join(p1: Path, ...others: string[]): Path; + +// @public (undocumented) +function joinJsonPointer(root: JsonPointer, ...others: string[]): JsonPointer; + +declare namespace json { + export { + schema, + isJsonObject, + isJsonArray, + JsonArray, + JsonObject, + JsonValue + } +} +export { json } + +// @public +export interface JsonArray extends Array { +} + +// @public (undocumented) +export interface JsonObject { + // (undocumented) + [prop: string]: JsonValue; +} + +// @public (undocumented) +type JsonPointer = string & { + __PRIVATE_DEVKIT_JSON_POINTER: void; +}; + +// @public +type JsonSchema = JsonObject | boolean; + +// @public (undocumented) +interface JsonSchemaVisitor { + // (undocumented) + (current: JsonObject | JsonArray, pointer: JsonPointer, parentSchema?: JsonObject | JsonArray, index?: string): void; +} + +// @public (undocumented) +export type JsonValue = boolean | string | number | JsonArray | JsonObject | null; + +// @public (undocumented) +interface JsonVisitor { + // (undocumented) + (value: JsonValue, pointer: JsonPointer, schema?: JsonObject, root?: JsonObject | JsonArray): Observable | JsonValue; +} + +// @public (undocumented) +class LevelCapLogger extends LevelTransformLogger { + constructor(name: string, parent: Logger | null, levelCap: LogLevel); + // (undocumented) + readonly levelCap: LogLevel; + // (undocumented) + static levelMap: { + [cap: string]: { + [level: string]: string; + }; + }; + // (undocumented) + readonly name: string; + // (undocumented) + readonly parent: Logger | null; +} + +// @public (undocumented) +class LevelTransformLogger extends Logger { + constructor(name: string, parent: Logger | null, levelTransform: (level: LogLevel) => LogLevel); + // (undocumented) + createChild(name: string): Logger; + // (undocumented) + readonly levelTransform: (level: LogLevel) => LogLevel; + // (undocumented) + log(level: LogLevel, message: string, metadata?: JsonObject): void; + // (undocumented) + readonly name: string; + // (undocumented) + readonly parent: Logger | null; +} + +// @public +function levenshtein(a: string, b: string): number; + +// @public (undocumented) +interface LogEntry extends LoggerMetadata { + // (undocumented) + level: LogLevel; + // (undocumented) + message: string; + // (undocumented) + timestamp: number; +} + +// @public (undocumented) +class Logger extends Observable implements LoggerApi { + constructor(name: string, parent?: Logger | null); + // (undocumented) + asApi(): LoggerApi; + // (undocumented) + complete(): void; + // (undocumented) + createChild(name: string): Logger; + // (undocumented) + debug(message: string, metadata?: JsonObject): void; + // (undocumented) + error(message: string, metadata?: JsonObject): void; + // (undocumented) + fatal(message: string, metadata?: JsonObject): void; + // (undocumented) + forEach(next: (value: LogEntry) => void, promiseCtor?: PromiseConstructorLike): Promise; + // (undocumented) + info(message: string, metadata?: JsonObject): void; + // (undocumented) + lift(operator: Operator): Observable; + // (undocumented) + log(level: LogLevel, message: string, metadata?: JsonObject): void; + // (undocumented) + protected _metadata: LoggerMetadata; + // (undocumented) + readonly name: string; + // (undocumented) + next(entry: LogEntry): void; + // (undocumented) + protected get _observable(): Observable; + protected set _observable(v: Observable); + // (undocumented) + readonly parent: Logger | null; + // (undocumented) + protected readonly _subject: Subject; + // (undocumented) + subscribe(): Subscription; + // (undocumented) + subscribe(observer: PartialObserver): Subscription; + // (undocumented) + subscribe(next?: (value: LogEntry) => void, error?: (error: Error) => void, complete?: () => void): Subscription; + // (undocumented) + toString(): string; + // (undocumented) + warn(message: string, metadata?: JsonObject): void; +} + +// @public (undocumented) +interface LoggerApi { + // (undocumented) + createChild(name: string): Logger; + // (undocumented) + debug(message: string, metadata?: JsonObject): void; + // (undocumented) + error(message: string, metadata?: JsonObject): void; + // (undocumented) + fatal(message: string, metadata?: JsonObject): void; + // (undocumented) + info(message: string, metadata?: JsonObject): void; + // (undocumented) + log(level: LogLevel, message: string, metadata?: JsonObject): void; + // (undocumented) + warn(message: string, metadata?: JsonObject): void; +} + +// @public (undocumented) +interface LoggerMetadata extends JsonObject { + // (undocumented) + name: string; + // (undocumented) + path: string[]; +} + +declare namespace logging { + export { + IndentLogger, + LevelTransformLogger, + LevelCapLogger, + LoggerMetadata, + LogEntry, + LoggerApi, + LogLevel, + Logger, + NullLogger, + TransformLogger + } +} +export { logging } + +// @public (undocumented) +type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'; + +// @public +function mergeSchemas(...schemas: (JsonSchema | undefined)[]): JsonSchema; + +// @public +export function noCacheNormalize(path: string): Path; + +// @public +export function normalize(path: string): Path; + +// @public +export const NormalizedRoot: Path; + +// @public +export const NormalizedSep: Path; + +// @public (undocumented) +class NullLogger extends Logger { + constructor(parent?: Logger | null); + // (undocumented) + asApi(): LoggerApi; +} + +// @public (undocumented) +function oneLine(strings: TemplateStringsArray, ...values: any[]): string; + +// @public (undocumented) +function parseJsonPointer(pointer: JsonPointer): string[]; + +// @public (undocumented) +export class PartiallyOrderedSet { + // (undocumented) + [Symbol.iterator](): IterableIterator; + // (undocumented) + get [Symbol.toStringTag](): 'PartiallyOrderedSet'; + // (undocumented) + add(item: T, deps?: Set | T[]): this; + // (undocumented) + protected _checkCircularDependencies(item: T, deps: Set): void; + // (undocumented) + clear(): void; + // (undocumented) + delete(item: T): boolean; + entries(): SetIterator<[T, T]>; + // (undocumented) + forEach(callbackfn: (value: T, value2: T, set: PartiallyOrderedSet) => void, thisArg?: any): void; + // (undocumented) + has(item: T): boolean; + keys(): SetIterator; + // (undocumented) + get size(): number; + values(): SetIterator; +} + +// @public +export type Path = string & { + __PRIVATE_DEVKIT_PATH: void; +}; + +// @public (undocumented) +export const path: TemplateTag; + +// @public (undocumented) +export class PathCannotBeFragmentException extends BaseException { + constructor(path: string); +} + +// @public +export type PathFragment = Path & { + __PRIVATE_DEVKIT_PATH_FRAGMENT: void; +}; + +// @public (undocumented) +export class PathIsDirectoryException extends BaseException { + constructor(path: string); +} + +// @public (undocumented) +export class PathIsFileException extends BaseException { + constructor(path: string); +} + +// @public (undocumented) +export class PathMustBeAbsoluteException extends BaseException { + constructor(path: string); +} + +// @public (undocumented) +class PatternMatchingHost extends ResolverHost { + // (undocumented) + addPattern(pattern: string | string[], replacementFn: ReplacementFunction): void; + // (undocumented) + protected _patterns: Map; + // (undocumented) + protected _resolve(path: Path): Path; +} + +// @public (undocumented) +export type PosixPath = string & { + __PRIVATE_DEVKIT_POSIX_PATH: void; +}; + +// @public +export class PriorityQueue { + constructor(_comparator: (x: T, y: T) => number); + // (undocumented) + clear(): void; + // (undocumented) + peek(): T | undefined; + // (undocumented) + pop(): T | undefined; + // (undocumented) + push(item: T): void; + // (undocumented) + get size(): number; + // (undocumented) + toArray(): Array; +} + +// @public (undocumented) +interface ProjectDefinition { + // (undocumented) + readonly extensions: Record; + // (undocumented) + prefix?: string; + // (undocumented) + root: string; + // (undocumented) + sourceRoot?: string; + // (undocumented) + readonly targets: TargetDefinitionCollection; +} + +// @public (undocumented) +class ProjectDefinitionCollection extends DefinitionCollection { + constructor(initial?: Record, listener?: DefinitionCollectionListener); + // (undocumented) + add(definition: { + name: string; + root: string; + sourceRoot?: string; + prefix?: string; + targets?: Record; + [key: string]: unknown; + }): ProjectDefinition; + // (undocumented) + set(name: string, value: ProjectDefinition): this; +} + +// @public (undocumented) +interface PromptDefinition { + // (undocumented) + default?: string | string[] | number | boolean | null; + // (undocumented) + id: string; + // (undocumented) + items?: Array; + // (undocumented) + message: string; + // (undocumented) + multiselect?: boolean; + // (undocumented) + propertyTypes: Set; + // (undocumented) + raw?: string | JsonObject; + // (undocumented) + type: string; + // (undocumented) + validator?: (value: JsonValue) => boolean | string | Promise; +} + +// @public (undocumented) +type PromptProvider = (definitions: Array) => ObservableInput<{ + [id: string]: JsonValue; +}>; + +// @public (undocumented) +interface ReadonlyHost { + // (undocumented) + readonly capabilities: HostCapabilities; + // (undocumented) + exists(path: Path): Observable; + // (undocumented) + isDirectory(path: Path): Observable; + // (undocumented) + isFile(path: Path): Observable; + // (undocumented) + list(path: Path): Observable; + // (undocumented) + read(path: Path): Observable; + // (undocumented) + stat(path: Path): Observable | null> | null; +} + +// @public +function readWorkspace(path: string, host: WorkspaceHost, format?: WorkspaceFormat): Promise<{ + workspace: WorkspaceDefinition; +}>; + +// @public (undocumented) +interface ReferenceResolver { + // (undocumented) + (ref: string, context?: ContextT): { + context?: ContextT; + schema?: JsonObject; + }; +} + +// @public +export function relative(from: Path, to: Path): Path; + +// @public (undocumented) +type ReplacementFunction = (path: Path) => Path; + +// @public +export function resetNormalizeCache(): void; + +// @public +export function resolve(p1: Path, p2: Path): Path; + +// @public +abstract class ResolverHost implements Host { + constructor(_delegate: Host); + // (undocumented) + get capabilities(): HostCapabilities; + // (undocumented) + protected _delegate: Host; + // (undocumented) + delete(path: Path): Observable; + // (undocumented) + exists(path: Path): Observable; + // (undocumented) + isDirectory(path: Path): Observable; + // (undocumented) + isFile(path: Path): Observable; + // (undocumented) + list(path: Path): Observable; + // (undocumented) + read(path: Path): Observable; + // (undocumented) + rename(from: Path, to: Path): Observable; + // (undocumented) + protected abstract _resolve(path: Path): Path; + // (undocumented) + stat(path: Path): Observable | null> | null; + // (undocumented) + watch(path: Path, options?: HostWatchOptions): Observable | null; + // (undocumented) + write(path: Path, content: FileBuffer): Observable; +} + +// @public +class SafeReadonlyHost implements ReadonlyHost { + constructor(_delegate: ReadonlyHost); + // (undocumented) + get capabilities(): HostCapabilities; + // (undocumented) + exists(path: Path): Observable; + // (undocumented) + isDirectory(path: Path): Observable; + // (undocumented) + isFile(path: Path): Observable; + // (undocumented) + list(path: Path): Observable; + // (undocumented) + read(path: Path): Observable; + // (undocumented) + stat(path: Path): Observable | null> | null; +} + +declare namespace schema { + export { + transforms, + JsonPointer, + SchemaValidatorResult, + SchemaValidatorError, + SchemaValidatorOptions, + SchemaValidator, + SchemaFormatter, + SchemaFormat, + SmartDefaultProvider, + SchemaKeywordValidator, + PromptDefinition, + PromptProvider, + SchemaRegistry, + JsonSchemaVisitor, + JsonVisitor, + buildJsonPointer, + joinJsonPointer, + parseJsonPointer, + UriHandler, + SchemaValidationException, + CoreSchemaRegistry, + isJsonSchema, + mergeSchemas, + JsonSchema, + visitJson, + visitJsonSchema, + ReferenceResolver, + getTypesOfSchema + } +} +export { schema } + +// @public (undocumented) +interface SchemaFormat { + // (undocumented) + formatter: SchemaFormatter; + // (undocumented) + name: string; +} + +// @public (undocumented) +type SchemaFormatter = Format; + +// @public (undocumented) +interface SchemaKeywordValidator { + // (undocumented) + (data: JsonValue, schema: JsonValue, parent: JsonObject | JsonArray | undefined, parentProperty: string | number | undefined, pointer: JsonPointer, rootData: JsonValue): boolean | Observable; +} + +// @public (undocumented) +interface SchemaRegistry { + // (undocumented) + addFormat(format: SchemaFormat): void; + addPostTransform(visitor: JsonVisitor, deps?: JsonVisitor[]): void; + addPreTransform(visitor: JsonVisitor, deps?: JsonVisitor[]): void; + // (undocumented) + addSmartDefaultProvider(source: string, provider: SmartDefaultProvider): void; + // (undocumented) + compile(schema: Object): Promise; + // (undocumented) + usePromptProvider(provider: PromptProvider): void; + // (undocumented) + useXDeprecatedProvider(onUsage: (message: string) => void): void; + // (undocumented) + ɵflatten(schema: JsonObject | string): Promise; +} + +// @public (undocumented) +class SchemaValidationException extends BaseException { + constructor(errors?: SchemaValidatorError[], baseMessage?: string); + // (undocumented) + static createMessages(errors?: SchemaValidatorError[]): string[]; + // (undocumented) + readonly errors: SchemaValidatorError[]; +} + +// @public (undocumented) +interface SchemaValidator { + // (undocumented) + (data: JsonValue, options?: SchemaValidatorOptions): Promise; +} + +// @public (undocumented) +type SchemaValidatorError = Partial; + +// @public (undocumented) +interface SchemaValidatorOptions { + // (undocumented) + applyPostTransforms?: boolean; + // (undocumented) + applyPreTransforms?: boolean; + // (undocumented) + withPrompts?: boolean; +} + +// @public (undocumented) +interface SchemaValidatorResult { + // (undocumented) + data: JsonValue; + // (undocumented) + errors?: SchemaValidatorError[]; + // (undocumented) + success: boolean; +} + +// @public (undocumented) +class ScopedHost extends ResolverHost { + constructor(delegate: Host, _root?: Path); + // (undocumented) + protected _resolve(path: Path): Path; + // (undocumented) + protected _root: Path; +} + +// @public (undocumented) +class SimpleMemoryHost implements Host<{}> { + constructor(); + // (undocumented) + protected _cache: Map>; + // (undocumented) + get capabilities(): HostCapabilities; + // (undocumented) + delete(path: Path): Observable; + // (undocumented) + protected _delete(path: Path): void; + // (undocumented) + exists(path: Path): Observable; + // (undocumented) + protected _exists(path: Path): boolean; + // (undocumented) + isDirectory(path: Path): Observable; + // (undocumented) + protected _isDirectory(path: Path): boolean; + // (undocumented) + isFile(path: Path): Observable; + // (undocumented) + protected _isFile(path: Path): boolean; + // (undocumented) + list(path: Path): Observable; + // (undocumented) + protected _list(path: Path): PathFragment[]; + // (undocumented) + protected _newDirStats(): Stats; + // (undocumented) + protected _newFileStats(content: FileBuffer, oldStats?: Stats): Stats; + // (undocumented) + read(path: Path): Observable; + // (undocumented) + protected _read(path: Path): FileBuffer; + // (undocumented) + rename(from: Path, to: Path): Observable; + // (undocumented) + protected _rename(from: Path, to: Path): void; + // (undocumented) + reset(): void; + // (undocumented) + stat(path: Path): Observable | null> | null; + // (undocumented) + protected _stat(path: Path): Stats | null; + // (undocumented) + protected _toAbsolute(path: Path): Path; + // (undocumented) + protected _updateWatchers(path: Path, type: HostWatchEventType): void; + // (undocumented) + watch(path: Path, options?: HostWatchOptions): Observable | null; + // (undocumented) + protected _watch(path: Path, options?: HostWatchOptions): Observable; + // (undocumented) + write(path: Path, content: FileBuffer): Observable; + protected _write(path: Path, content: FileBuffer): void; +} + +// @public (undocumented) +interface SimpleMemoryHostStats { + // (undocumented) + readonly content: FileBuffer | null; + // (undocumented) + inspect(): string; +} + +// @public (undocumented) +interface SmartDefaultProvider { + // (undocumented) + (schema: JsonObject): T | Observable; +} + +// @public +export function split(path: Path): PathFragment[]; + +// @public (undocumented) +type Stats = T & { + isFile(): boolean; + isDirectory(): boolean; + readonly size: number; + readonly atime: Date; + readonly mtime: Date; + readonly ctime: Date; + readonly birthtime: Date; +}; + +declare namespace strings { + export { + decamelize, + dasherize, + camelize, + classify, + underscore, + capitalize, + levenshtein + } +} +export { strings } + +// @public (undocumented) +function stringToFileBuffer(str: string): FileBuffer; + +// @public (undocumented) +function stripIndent(strings: TemplateStringsArray, ...values: any[]): string; + +// @public (undocumented) +function stripIndents(strings: TemplateStringsArray, ...values: any[]): string; + +// @public +class SyncDelegateHost { + constructor(_delegate: Host); + // (undocumented) + get capabilities(): HostCapabilities; + // (undocumented) + get delegate(): Host; + // (undocumented) + protected _delegate: Host; + // (undocumented) + delete(path: Path): void; + // (undocumented) + protected _doSyncCall(observable: Observable): ResultT; + // (undocumented) + exists(path: Path): boolean; + // (undocumented) + isDirectory(path: Path): boolean; + // (undocumented) + isFile(path: Path): boolean; + // (undocumented) + list(path: Path): PathFragment[]; + // (undocumented) + read(path: Path): FileBuffer; + // (undocumented) + rename(from: Path, to: Path): void; + // (undocumented) + stat(path: Path): Stats | null; + // (undocumented) + watch(path: Path, options?: HostWatchOptions): Observable | null; + // (undocumented) + write(path: Path, content: FileBufferLike): void; +} + +// @public (undocumented) +interface SyncHostHandler { + // (undocumented) + delete(path: Path): void; + // (undocumented) + exists(path: Path): boolean; + // (undocumented) + isDirectory(path: Path): boolean; + // (undocumented) + isFile(path: Path): boolean; + // (undocumented) + list(path: Path): PathFragment[]; + // (undocumented) + read(path: Path): FileBuffer; + // (undocumented) + rename(from: Path, to: Path): void; + // (undocumented) + stat(path: Path): Stats | null; + // (undocumented) + write(path: Path, content: FileBufferLike): void; +} + +// @public (undocumented) +class SynchronousDelegateExpectedException extends BaseException { + constructor(); +} + +declare namespace tags { + export { + oneLine, + indentBy, + stripIndent, + stripIndents, + trimNewlines, + TemplateTag + } +} +export { tags } + +// @public (undocumented) +interface TargetDefinition { + // (undocumented) + builder: string; + // (undocumented) + configurations?: Record | undefined>; + // (undocumented) + defaultConfiguration?: string; + // (undocumented) + options?: Record; +} + +// @public (undocumented) +class TargetDefinitionCollection extends DefinitionCollection { + constructor(initial?: Record, listener?: DefinitionCollectionListener); + // (undocumented) + add(definition: { + name: string; + } & TargetDefinition): TargetDefinition; + // (undocumented) + set(name: string, value: TargetDefinition): this; +} + +// @public +export function template(content: string, options?: TemplateOptions): (input: T) => string; + +// @public +export interface TemplateAst { + // (undocumented) + children: TemplateAstNode[]; + // (undocumented) + content: string; + // (undocumented) + fileName: string; +} + +// @public +export interface TemplateAstBase { + // (undocumented) + end: Position; + // (undocumented) + start: Position; +} + +// @public +export interface TemplateAstComment extends TemplateAstBase { + // (undocumented) + kind: 'comment'; + // (undocumented) + text: string; +} + +// @public +export interface TemplateAstContent extends TemplateAstBase { + // (undocumented) + content: string; + // (undocumented) + kind: 'content'; +} + +// @public +export interface TemplateAstEscape extends TemplateAstBase { + // (undocumented) + expression: string; + // (undocumented) + kind: 'escape'; +} + +// @public +export interface TemplateAstEvaluate extends TemplateAstBase { + // (undocumented) + expression: string; + // (undocumented) + kind: 'evaluate'; +} + +// @public +export interface TemplateAstInterpolate extends TemplateAstBase { + // (undocumented) + expression: string; + // (undocumented) + kind: 'interpolate'; +} + +// @public (undocumented) +export type TemplateAstNode = TemplateAstContent | TemplateAstEvaluate | TemplateAstComment | TemplateAstEscape | TemplateAstInterpolate; + +// @public (undocumented) +export interface TemplateOptions { + // (undocumented) + fileName?: string; + // (undocumented) + module?: boolean | { + exports: {}; + }; + // (undocumented) + sourceMap?: boolean; + // (undocumented) + sourceRoot?: string; + // (undocumented) + sourceURL?: string; +} + +// @public +export function templateParser(sourceText: string, fileName: string): TemplateAst; + +// @public +interface TemplateTag { + // (undocumented) + (template: TemplateStringsArray, ...substitutions: any[]): R; +} + +declare namespace test { + export { + TestLogRecord, + TestHost + } +} + +// @public (undocumented) +class TestHost extends SimpleMemoryHost { + // (undocumented) + $exists(path: string): boolean; + // (undocumented) + $isDirectory(path: string): boolean; + // (undocumented) + $isFile(path: string): boolean; + // (undocumented) + $list(path: string): PathFragment[]; + // (undocumented) + $read(path: string): string; + // (undocumented) + $write(path: string, content: string): void; + constructor(map?: { + [path: string]: string; + }); + // (undocumented) + clearRecords(): void; + // (undocumented) + clone(): TestHost; + // (undocumented) + protected _delete(path: Path): void; + // (undocumented) + protected _exists(path: Path): boolean; + // (undocumented) + get files(): Path[]; + // (undocumented) + protected _isDirectory(path: Path): boolean; + // (undocumented) + protected _isFile(path: Path): boolean; + // (undocumented) + protected _list(path: Path): PathFragment[]; + // (undocumented) + protected _read(path: Path): ArrayBuffer; + // (undocumented) + get records(): TestLogRecord[]; + // (undocumented) + protected _records: TestLogRecord[]; + // (undocumented) + protected _rename(from: Path, to: Path): void; + // (undocumented) + protected _stat(path: Path): Stats | null; + // (undocumented) + get sync(): SyncDelegateHost<{}>; + // (undocumented) + protected _sync: SyncDelegateHost<{}> | null; + // (undocumented) + protected _watch(path: Path, options?: HostWatchOptions): Observable; + // (undocumented) + protected _write(path: Path, content: FileBuffer): void; +} + +// @public (undocumented) +type TestLogRecord = { + kind: 'write' | 'read' | 'delete' | 'list' | 'exists' | 'isDirectory' | 'isFile' | 'stat' | 'watch'; + path: Path; +} | { + kind: 'rename'; + from: Path; + to: Path; +}; + +// @public (undocumented) +class TransformLogger extends Logger { + constructor(name: string, transform: (stream: Observable) => Observable, parent?: Logger | null); +} + +declare namespace transforms { + export { + addUndefinedObjectDefaults, + addUndefinedDefaults + } +} + +// @public (undocumented) +function trimNewlines(strings: TemplateStringsArray, ...values: any[]): string; + +// @public +function underscore(str: string): string; + +// @public (undocumented) +export class UnknownException extends BaseException { + constructor(message: string); +} + +// @public (undocumented) +type UriHandler = (uri: string) => Observable | Promise | null | undefined; + +declare namespace virtualFs { + export { + test, + AliasHost, + stringToFileBuffer, + fileBufferToString, + createSyncHost, + SyncHostHandler, + Empty, + FileBuffer, + FileBufferLike, + HostWatchOptions, + HostWatchEventType, + Stats, + HostWatchEvent, + HostCapabilities, + ReadonlyHost, + Host, + SimpleMemoryHostStats, + SimpleMemoryHost, + ReplacementFunction, + PatternMatchingHost, + CordHostCreate, + CordHostOverwrite, + CordHostRename, + CordHostDelete, + CordHostRecord, + CordHost, + SafeReadonlyHost, + ScopedHost, + SynchronousDelegateExpectedException, + SyncDelegateHost, + ResolverHost + } +} +export { virtualFs } + +// @public +function visitJson(json: JsonValue, visitor: JsonVisitor, schema?: JsonSchema, refResolver?: ReferenceResolver, context?: ContextT): Observable; + +// @public (undocumented) +function visitJsonSchema(schema: JsonSchema, visitor: JsonSchemaVisitor): void; + +// @public (undocumented) +export type WindowsPath = string & { + __PRIVATE_DEVKIT_WINDOWS_PATH: void; +}; + +// @public (undocumented) +interface WorkspaceDefinition { + // (undocumented) + readonly extensions: Record; + // (undocumented) + readonly projects: ProjectDefinitionCollection; +} + +// @public +enum WorkspaceFormat { + // (undocumented) + JSON = 0 +} + +// @public (undocumented) +interface WorkspaceHost { + // (undocumented) + isDirectory(path: string): Promise; + // (undocumented) + isFile(path: string): Promise; + // (undocumented) + readFile(path: string): Promise; + // (undocumented) + writeFile(path: string, data: string): Promise; +} + +declare namespace workspaces { + export { + WorkspaceHost, + createWorkspaceHost, + WorkspaceFormat, + readWorkspace, + writeWorkspace, + WorkspaceDefinition, + ProjectDefinition, + TargetDefinition, + DefinitionCollectionListener, + ProjectDefinitionCollection, + TargetDefinitionCollection + } +} +export { workspaces } + +// @public +function writeWorkspace(workspace: WorkspaceDefinition, host: WorkspaceHost, path?: string, format?: WorkspaceFormat): Promise; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular_devkit/core/node/index.api.md b/goldens/public-api/angular_devkit/core/node/index.api.md new file mode 100644 index 000000000000..cb18462521ca --- /dev/null +++ b/goldens/public-api/angular_devkit/core/node/index.api.md @@ -0,0 +1,77 @@ +## API Report File for "@angular-devkit/core_node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Observable } from 'rxjs'; +import { Operator } from 'rxjs'; +import { PartialObserver } from 'rxjs'; +import { Stats as Stats_2 } from 'node:fs'; +import { Subject } from 'rxjs'; +import { Subscription } from 'rxjs'; + +// @public +export function createConsoleLogger(verbose?: boolean, stdout?: ProcessOutput, stderr?: ProcessOutput, colors?: Partial string>>): logging.Logger; + +// @public +export class NodeJsAsyncHost implements virtualFs.Host { + // (undocumented) + get capabilities(): virtualFs.HostCapabilities; + // (undocumented) + delete(path: Path): Observable; + // (undocumented) + exists(path: Path): Observable; + // (undocumented) + isDirectory(path: Path): Observable; + // (undocumented) + isFile(path: Path): Observable; + // (undocumented) + list(path: Path): Observable; + // (undocumented) + read(path: Path): Observable; + // (undocumented) + rename(from: Path, to: Path): Observable; + // (undocumented) + stat(path: Path): Observable>; + // (undocumented) + watch(path: Path, _options?: virtualFs.HostWatchOptions): Observable | null; + // (undocumented) + write(path: Path, content: virtualFs.FileBuffer): Observable; +} + +// @public +export class NodeJsSyncHost implements virtualFs.Host { + // (undocumented) + get capabilities(): virtualFs.HostCapabilities; + // (undocumented) + delete(path: Path): Observable; + // (undocumented) + exists(path: Path): Observable; + // (undocumented) + isDirectory(path: Path): Observable; + // (undocumented) + isFile(path: Path): Observable; + // (undocumented) + list(path: Path): Observable; + // (undocumented) + read(path: Path): Observable; + // (undocumented) + rename(from: Path, to: Path): Observable; + // (undocumented) + stat(path: Path): Observable>; + // (undocumented) + watch(path: Path, _options?: virtualFs.HostWatchOptions): Observable | null; + // (undocumented) + write(path: Path, content: virtualFs.FileBuffer): Observable; +} + +// @public (undocumented) +export interface ProcessOutput { + // (undocumented) + write(buffer: string | Buffer): boolean; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular_devkit/core/node/testing/index.api.md b/goldens/public-api/angular_devkit/core/node/testing/index.api.md new file mode 100644 index 000000000000..116019d266a1 --- /dev/null +++ b/goldens/public-api/angular_devkit/core/node/testing/index.api.md @@ -0,0 +1,27 @@ +## API Report File for "@angular-devkit/core_node_testing" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as fs from 'node:fs'; +import { Observable } from 'rxjs'; + +// @public +export class TempScopedNodeJsSyncHost extends virtualFs.ScopedHost { + constructor(); + // (undocumented) + get files(): Path[]; + // (undocumented) + get root(): Path; + // (undocumented) + protected _root: Path; + // (undocumented) + get sync(): virtualFs.SyncDelegateHost; + // (undocumented) + protected _sync?: virtualFs.SyncDelegateHost; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular_devkit/schematics/index.api.md b/goldens/public-api/angular_devkit/schematics/index.api.md new file mode 100644 index 000000000000..add7132d138f --- /dev/null +++ b/goldens/public-api/angular_devkit/schematics/index.api.md @@ -0,0 +1,1054 @@ +## API Report File for "@angular-devkit/schematics" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BaseException } from '@angular-devkit/core'; +import { JsonValue } from '@angular-devkit/core'; +import { logging } from '@angular-devkit/core'; +import { Observable } from 'rxjs'; +import { Path } from '@angular-devkit/core'; +import { PathFragment } from '@angular-devkit/core'; +import { schema } from '@angular-devkit/core'; +import { strings } from '@angular-devkit/core'; +import { Subject } from 'rxjs'; +import { Url } from 'node:url'; +import { virtualFs } from '@angular-devkit/core'; + +// @public (undocumented) +export type Action = CreateFileAction | OverwriteFileAction | RenameFileAction | DeleteFileAction; + +// @public (undocumented) +export interface ActionBase { + // (undocumented) + readonly id: number; + // (undocumented) + readonly parent: number; + // (undocumented) + readonly path: Path; +} + +// @public (undocumented) +export class ActionList implements Iterable { + // (undocumented) + [Symbol.iterator](): IterableIterator; + // (undocumented) + protected _action(action: Partial): void; + // (undocumented) + create(path: Path, content: Buffer): void; + // (undocumented) + delete(path: Path): void; + // (undocumented) + find(predicate: (value: Action) => boolean): Action | null; + // (undocumented) + forEach(fn: (value: Action, index: number, array: Action[]) => void, thisArg?: {}): void; + // (undocumented) + get(i: number): Action; + // (undocumented) + has(action: Action): boolean; + // (undocumented) + get length(): number; + // (undocumented) + optimize(): void; + // (undocumented) + overwrite(path: Path, content: Buffer): void; + // (undocumented) + push(action: Action): void; + // (undocumented) + rename(path: Path, to: Path): void; +} + +// @public +export function apply(source: Source, rules: Rule[]): Source; + +// @public (undocumented) +export function applyContentTemplate(options: T): FileOperator; + +// @public (undocumented) +export function applyPathTemplate(data: T, options?: PathTemplateOptions): FileOperator; + +// @public (undocumented) +export function applyTemplates(options: T): Rule; + +// @public (undocumented) +export function applyToSubtree(path: string, rules: Rule[]): Rule; + +// @public (undocumented) +export function asSource(rule: Rule): Source; + +// @public (undocumented) +export type AsyncFileOperator = (tree: FileEntry) => Observable; + +// @public +abstract class BaseWorkflow implements Workflow { + constructor(options: BaseWorkflowOptions); + // (undocumented) + get context(): Readonly; + // (undocumented) + protected _context: WorkflowExecutionContext[]; + // (undocumented) + protected _createSinks(): Sink[]; + // (undocumented) + protected _dryRun: boolean; + // (undocumented) + get engine(): Engine<{}, {}>; + // (undocumented) + protected _engine: Engine<{}, {}>; + // (undocumented) + get engineHost(): EngineHost<{}, {}>; + // (undocumented) + protected _engineHost: EngineHost<{}, {}>; + // (undocumented) + execute(options: Partial & RequiredWorkflowExecutionContext): Observable; + // (undocumented) + protected _force: boolean; + // (undocumented) + protected _host: virtualFs.Host; + // (undocumented) + get lifeCycle(): Observable; + // (undocumented) + protected _lifeCycle: Subject; + // (undocumented) + get registry(): schema.SchemaRegistry; + // (undocumented) + protected _registry: schema.CoreSchemaRegistry; + // (undocumented) + get reporter(): Observable; + // (undocumented) + protected _reporter: Subject; +} + +// @public (undocumented) +interface BaseWorkflowOptions { + // (undocumented) + dryRun?: boolean; + // (undocumented) + engineHost: EngineHost<{}, {}>; + // (undocumented) + force?: boolean; + // (undocumented) + host: virtualFs.Host; + // (undocumented) + registry?: schema.CoreSchemaRegistry; +} + +// @public (undocumented) +export function branchAndMerge(rule: Rule, strategy?: MergeStrategy): Rule; + +// @public (undocumented) +export function callRule(rule: Rule, input: Tree_2 | Observable, context: SchematicContext): Observable; + +// @public (undocumented) +export function callSource(source: Source, context: SchematicContext): Observable; + +// @public +export function chain(rules: Iterable | AsyncIterable): Rule; + +// @public (undocumented) +export class CircularCollectionException extends BaseException { + constructor(name: string); +} + +// @public +export interface Collection { + // (undocumented) + readonly baseDescriptions?: Array>; + // (undocumented) + createSchematic(name: string, allowPrivate?: boolean): Schematic; + // (undocumented) + readonly description: CollectionDescription; + // (undocumented) + listSchematicNames(includeHidden?: boolean): string[]; +} + +// @public +export type CollectionDescription = CollectionMetadataT & { + readonly name: string; + readonly extends?: string[]; +}; + +// @public (undocumented) +export class CollectionImpl implements Collection { + constructor(_description: CollectionDescription, _engine: SchematicEngine, baseDescriptions?: Array> | undefined); + // (undocumented) + readonly baseDescriptions?: Array> | undefined; + // (undocumented) + createSchematic(name: string, allowPrivate?: boolean): Schematic; + // (undocumented) + get description(): CollectionDescription; + // (undocumented) + listSchematicNames(includeHidden?: boolean): string[]; + // (undocumented) + get name(): string; +} + +// @public (undocumented) +export function composeFileOperators(operators: FileOperator[]): FileOperator; + +// @public (undocumented) +export class ContentHasMutatedException extends BaseException { + constructor(path: string); +} + +// @public (undocumented) +export function contentTemplate(options: T): Rule; + +// @public (undocumented) +export interface CreateFileAction extends ActionBase { + // (undocumented) + readonly content: Buffer; + // (undocumented) + readonly kind: 'c'; +} + +// @public (undocumented) +export class DelegateTree implements Tree_2 { + // (undocumented) + [TreeSymbol]: () => this; + constructor(_other: Tree_2); + // (undocumented) + get actions(): Action[]; + // (undocumented) + apply(action: Action, strategy?: MergeStrategy): void; + // (undocumented) + beginUpdate(path: string): UpdateRecorder; + // (undocumented) + branch(): Tree_2; + // (undocumented) + commitUpdate(record: UpdateRecorder): void; + // (undocumented) + create(path: string, content: Buffer | string): void; + // (undocumented) + delete(path: string): void; + // (undocumented) + exists(path: string): boolean; + // (undocumented) + get(path: string): FileEntry | null; + // (undocumented) + getDir(path: string): DirEntry; + // (undocumented) + merge(other: Tree_2, strategy?: MergeStrategy): void; + // (undocumented) + protected _other: Tree_2; + // (undocumented) + overwrite(path: string, content: Buffer | string): void; + // (undocumented) + read(path: string): Buffer | null; + // (undocumented) + readJson(path: string): JsonValue; + // (undocumented) + readText(path: string): string; + // (undocumented) + rename(from: string, to: string): void; + // (undocumented) + get root(): DirEntry; + // (undocumented) + visit(visitor: FileVisitor): void; +} + +// @public (undocumented) +export interface DeleteFileAction extends ActionBase { + // (undocumented) + readonly kind: 'd'; +} + +// @public (undocumented) +export interface DirEntry { + // (undocumented) + dir(name: PathFragment): DirEntry; + // (undocumented) + file(name: PathFragment): FileEntry | null; + // (undocumented) + readonly parent: DirEntry | null; + // (undocumented) + readonly path: Path; + // (undocumented) + readonly subdirs: PathFragment[]; + // (undocumented) + readonly subfiles: PathFragment[]; + // (undocumented) + visit(visitor: FileVisitor): void; +} + +// @public (undocumented) +export interface DryRunCreateEvent { + // (undocumented) + content: Buffer; + // (undocumented) + kind: 'create'; + // (undocumented) + path: string; +} + +// @public (undocumented) +export interface DryRunDeleteEvent { + // (undocumented) + kind: 'delete'; + // (undocumented) + path: string; +} + +// @public (undocumented) +export interface DryRunErrorEvent { + // (undocumented) + description: 'alreadyExist' | 'doesNotExist'; + // (undocumented) + kind: 'error'; + // (undocumented) + path: string; +} + +// @public (undocumented) +export type DryRunEvent = DryRunErrorEvent | DryRunDeleteEvent | DryRunCreateEvent | DryRunUpdateEvent | DryRunRenameEvent; + +// @public (undocumented) +export interface DryRunRenameEvent { + // (undocumented) + kind: 'rename'; + // (undocumented) + path: string; + // (undocumented) + to: string; +} + +// @public (undocumented) +export class DryRunSink extends HostSink { + constructor(host: virtualFs.Host, force?: boolean); + // (undocumented) + _done(): Observable; + // (undocumented) + protected _fileAlreadyExistException(path: string): void; + // (undocumented) + protected _fileAlreadyExistExceptionSet: Set; + // (undocumented) + protected _fileDoesNotExistException(path: string): void; + // (undocumented) + protected _fileDoesNotExistExceptionSet: Set; + // (undocumented) + readonly reporter: Observable; + // (undocumented) + protected _subject: Subject; +} + +// @public (undocumented) +export interface DryRunUpdateEvent { + // (undocumented) + content: Buffer; + // (undocumented) + kind: 'update'; + // (undocumented) + path: string; +} + +// @public +export function empty(): Source; + +// @public (undocumented) +export class EmptyTree extends HostTree { + constructor(); +} + +// @public +export interface Engine { + // (undocumented) + createCollection(name: string, requester?: Collection): Collection; + // (undocumented) + createContext(schematic: Schematic, parent?: Partial>, executionOptions?: Partial): TypedSchematicContext; + // (undocumented) + createSchematic(name: string, collection: Collection): Schematic; + // (undocumented) + createSourceFromUrl(url: Url, context: TypedSchematicContext): Source; + // (undocumented) + readonly defaultMergeStrategy: MergeStrategy; + // (undocumented) + executePostTasks(): Observable; + // (undocumented) + transformOptions(schematic: Schematic, options: OptionT, context?: TypedSchematicContext): Observable; + // (undocumented) + readonly workflow: Workflow | null; +} + +// @public +export interface EngineHost { + // (undocumented) + createCollectionDescription(name: string, requester?: CollectionDescription): CollectionDescription; + // (undocumented) + createSchematicDescription(name: string, collection: CollectionDescription): SchematicDescription | null; + // (undocumented) + createSourceFromUrl(url: Url, context: TypedSchematicContext): Source | null; + // (undocumented) + createTaskExecutor(name: string): Observable; + // (undocumented) + readonly defaultMergeStrategy?: MergeStrategy; + // (undocumented) + getSchematicRuleFactory(schematic: SchematicDescription, collection: CollectionDescription): RuleFactory; + // (undocumented) + hasTaskExecutor(name: string): boolean; + // (undocumented) + listSchematicNames(collection: CollectionDescription, includeHidden?: boolean): string[]; + // (undocumented) + transformContext(context: TypedSchematicContext): TypedSchematicContext | void; + // (undocumented) + transformOptions(schematic: SchematicDescription, options: OptionT, context?: TypedSchematicContext): Observable; +} + +// @public (undocumented) +export interface ExecutionOptions { + // (undocumented) + interactive: boolean; + // (undocumented) + scope: string; +} + +// @public +export function externalSchematic(collectionName: string, schematicName: string, options: OptionT, executionOptions?: Partial): Rule; + +// @public (undocumented) +export class FileAlreadyExistException extends BaseException { + constructor(path: string); +} + +// @public (undocumented) +export class FileDoesNotExistException extends BaseException { + constructor(path: string); +} + +// @public (undocumented) +export interface FileEntry { + // (undocumented) + readonly content: Buffer; + // (undocumented) + readonly path: Path; +} + +// @public +export type FileOperator = (entry: FileEntry) => FileEntry | null; + +// @public (undocumented) +export interface FilePredicate { + // (undocumented) + (path: Path, entry?: Readonly | null): T; +} + +// @public (undocumented) +export type FileVisitor = FilePredicate; + +// @public (undocumented) +export const FileVisitorCancelToken: symbol; + +// @public (undocumented) +export function filter(predicate: FilePredicate): Rule; + +// @public (undocumented) +export class FilterHostTree extends HostTree { + constructor(tree: HostTree, filter?: FilePredicate); +} + +// @public (undocumented) +export function forEach(operator: FileOperator): Rule; + +declare namespace formats { + export { + htmlSelectorFormat, + pathFormat, + standardFormats + } +} +export { formats } + +// @public (undocumented) +export class HostCreateTree extends HostTree { + constructor(host: virtualFs.ReadonlyHost); +} + +// @public (undocumented) +export class HostDirEntry implements DirEntry { + constructor(parent: DirEntry | null, path: Path, _host: virtualFs.SyncDelegateHost, _tree: Tree_2); + // (undocumented) + dir(name: PathFragment): DirEntry; + // (undocumented) + file(name: PathFragment): FileEntry | null; + // (undocumented) + protected _host: virtualFs.SyncDelegateHost; + // (undocumented) + readonly parent: DirEntry | null; + // (undocumented) + readonly path: Path; + // (undocumented) + get subdirs(): PathFragment[]; + // (undocumented) + get subfiles(): PathFragment[]; + // (undocumented) + protected _tree: Tree_2; + // (undocumented) + visit(visitor: FileVisitor): void; +} + +// @public (undocumented) +export class HostSink extends SimpleSinkBase { + constructor(_host: virtualFs.Host, _force?: boolean); + // (undocumented) + protected _createFile(path: Path, content: Buffer): Observable; + // (undocumented) + protected _deleteFile(path: Path): Observable; + // (undocumented) + _done(): Observable; + // (undocumented) + protected _filesToCreate: Map>; + // (undocumented) + protected _filesToDelete: Set; + // (undocumented) + protected _filesToRename: Set<[Path, Path]>; + // (undocumented) + protected _filesToUpdate: Map>; + // (undocumented) + protected _force: boolean; + // (undocumented) + protected _host: virtualFs.Host; + // (undocumented) + protected _overwriteFile(path: Path, content: Buffer): Observable; + // (undocumented) + protected _renameFile(from: Path, to: Path): Observable; + // (undocumented) + protected _validateCreateAction(action: CreateFileAction): Observable; + // (undocumented) + protected _validateFileExists(p: Path): Observable; +} + +// @public (undocumented) +export class HostTree implements Tree_2 { + // (undocumented) + [TreeSymbol]: () => this; + constructor(_backend?: virtualFs.ReadonlyHost<{}>); + // (undocumented) + get actions(): Action[]; + // (undocumented) + apply(action: Action, strategy?: MergeStrategy): void; + // (undocumented) + protected _backend: virtualFs.ReadonlyHost<{}>; + // (undocumented) + beginUpdate(path: string): UpdateRecorder; + // (undocumented) + branch(): Tree_2; + // (undocumented) + commitUpdate(record: UpdateRecorder): void; + // (undocumented) + create(path: string, content: Buffer | string): void; + // (undocumented) + delete(path: string): void; + // (undocumented) + exists(path: string): boolean; + // (undocumented) + get(path: string): FileEntry | null; + // (undocumented) + getDir(path: string): DirEntry; + // (undocumented) + static isHostTree(tree: Tree_2): tree is HostTree; + // (undocumented) + merge(other: Tree_2, strategy?: MergeStrategy): void; + // (undocumented) + protected _normalizePath(path: string): Path; + // (undocumented) + overwrite(path: string, content: Buffer | string): void; + // (undocumented) + read(path: string): Buffer | null; + // (undocumented) + readJson(path: string): JsonValue; + // (undocumented) + readText(path: string): string; + // (undocumented) + rename(from: string, to: string): void; + // (undocumented) + get root(): DirEntry; + // (undocumented) + visit(visitor: FileVisitor): void; + // (undocumented) + protected _willCreate(path: Path): boolean; + // (undocumented) + protected _willDelete(path: Path): boolean; + // (undocumented) + protected _willOverwrite(path: Path): boolean; + // (undocumented) + protected _willRename(path: Path): boolean; +} + +// @public (undocumented) +const htmlSelectorFormat: schema.SchemaFormat; + +// @public (undocumented) +export class InvalidPipeException extends BaseException { + constructor(name: string); +} + +// @public +export class InvalidRuleResultException extends BaseException { + constructor(value?: {}); +} + +// @public (undocumented) +export class InvalidSchematicsNameException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export class InvalidSourceResultException extends BaseException { + constructor(value?: {}); +} + +// @public (undocumented) +export class InvalidUpdateRecordException extends BaseException { + constructor(); +} + +// @public (undocumented) +export function isContentAction(action: Action): action is CreateFileAction | OverwriteFileAction; + +// @public (undocumented) +interface LifeCycleEvent { + // (undocumented) + kind: 'start' | 'end' | 'workflow-start' | 'workflow-end' | 'post-tasks-start' | 'post-tasks-end'; +} + +// @public (undocumented) +export class MergeConflictException extends BaseException { + constructor(path: string); +} + +// @public (undocumented) +export enum MergeStrategy { + // (undocumented) + AllowCreationConflict = 4, + // (undocumented) + AllowDeleteConflict = 8, + // (undocumented) + AllowOverwriteConflict = 2, + // (undocumented) + ContentOnly = 2, + // (undocumented) + Default = 0, + // (undocumented) + Error = 1, + // (undocumented) + Overwrite = 14 +} + +// @public +export function mergeWith(source: Source, strategy?: MergeStrategy): Rule; + +// @public (undocumented) +export function move(from: string, to?: string): Rule; + +// @public (undocumented) +export function noop(): Rule; + +// @public (undocumented) +export class OptionIsNotDefinedException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export interface OverwriteFileAction extends ActionBase { + // (undocumented) + readonly content: Buffer; + // (undocumented) + readonly kind: 'o'; +} + +// @public (undocumented) +export function partitionApplyMerge(predicate: FilePredicate, ruleYes: Rule, ruleNo?: Rule): Rule; + +// @public (undocumented) +const pathFormat: schema.SchemaFormat; + +// @public (undocumented) +export function pathTemplate(options: T): Rule; + +// @public (undocumented) +export type PathTemplateData = { + [key: string]: PathTemplateValue | PathTemplateData | PathTemplatePipeFunction; +}; + +// @public (undocumented) +export interface PathTemplateOptions { + // (undocumented) + interpolationEnd: string; + // (undocumented) + interpolationStart: string; + // (undocumented) + pipeSeparator?: string; +} + +// @public (undocumented) +export type PathTemplatePipeFunction = (x: string) => PathTemplateValue; + +// @public (undocumented) +export type PathTemplateValue = boolean | string | number | undefined; + +// @public (undocumented) +export class PrivateSchematicException extends BaseException { + constructor(name: string, collection: CollectionDescription<{}>); +} + +// @public (undocumented) +export interface RandomOptions { + // (undocumented) + multi?: boolean | number; + // (undocumented) + multiFiles?: boolean | number; + // (undocumented) + root?: string; +} + +// @public (undocumented) +export interface RenameFileAction extends ActionBase { + // (undocumented) + readonly kind: 'r'; + // (undocumented) + readonly to: Path; +} + +// @public +export function renameTemplateFiles(): Rule; + +// @public (undocumented) +interface RequiredWorkflowExecutionContext { + // (undocumented) + collection: string; + // (undocumented) + options: object; + // (undocumented) + schematic: string; +} + +// @public (undocumented) +export type Rule = (tree: Tree_2, context: SchematicContext) => Tree_2 | Observable | Rule | Promise | void; + +// @public +export type RuleFactory = (options: T) => Rule; + +// @public +export interface Schematic { + // (undocumented) + call(options: OptionT, host: Observable, parentContext?: Partial>, executionOptions?: Partial): Observable; + // (undocumented) + readonly collection: Collection; + // (undocumented) + readonly description: SchematicDescription; +} + +// @public +export function schematic(schematicName: string, options: OptionT, executionOptions?: Partial): Rule; + +// @public +export type SchematicContext = TypedSchematicContext<{}, {}>; + +// @public +export type SchematicDescription = SchematicMetadataT & { + readonly collection: CollectionDescription; + readonly name: string; + readonly private?: boolean; + readonly hidden?: boolean; +}; + +// @public (undocumented) +export class SchematicEngine implements Engine { + constructor(_host: EngineHost, _workflow?: Workflow | undefined); + // (undocumented) + createCollection(name: string, requester?: Collection): Collection; + // (undocumented) + createContext(schematic: Schematic, parent?: Partial>, executionOptions?: Partial): TypedSchematicContext; + // (undocumented) + createSchematic(name: string, collection: Collection, allowPrivate?: boolean): Schematic; + // (undocumented) + createSourceFromUrl(url: Url, context: TypedSchematicContext): Source; + // (undocumented) + get defaultMergeStrategy(): MergeStrategy; + // (undocumented) + executePostTasks(): Observable; + // (undocumented) + listSchematicNames(collection: Collection, includeHidden?: boolean): string[]; + // (undocumented) + transformOptions(schematic: Schematic, options: OptionT, context?: TypedSchematicContext): Observable; + // (undocumented) + get workflow(): Workflow | null; + // (undocumented) + protected _workflow?: Workflow | undefined; +} + +// @public (undocumented) +export class SchematicEngineConflictingException extends BaseException { + constructor(); +} + +// @public (undocumented) +export class SchematicImpl implements Schematic { + constructor(_description: SchematicDescription, _factory: RuleFactory<{}>, _collection: Collection, _engine: Engine); + // (undocumented) + call(options: OptionT, host: Observable, parentContext?: Partial>, executionOptions?: Partial): Observable; + // (undocumented) + get collection(): Collection; + // (undocumented) + get description(): SchematicDescription; +} + +// @public (undocumented) +export class SchematicsException extends BaseException { +} + +// @public (undocumented) +export abstract class SimpleSinkBase implements Sink { + // (undocumented) + commit(tree: Tree_2): Observable; + // (undocumented) + commitSingleAction(action: Action): Observable; + // (undocumented) + protected abstract _createFile(path: string, content: Buffer): Observable; + // (undocumented) + protected abstract _deleteFile(path: string): Observable; + // (undocumented) + protected abstract _done(): Observable; + // (undocumented) + protected _fileAlreadyExistException(path: string): void; + // (undocumented) + protected _fileDoesNotExistException(path: string): void; + // (undocumented) + protected abstract _overwriteFile(path: string, content: Buffer): Observable; + // (undocumented) + postCommit: () => void | Observable; + // (undocumented) + postCommitAction: (action: Action) => void | Observable; + // (undocumented) + preCommit: () => void | Observable; + // (undocumented) + preCommitAction: (action: Action) => void | Action | PromiseLike | Observable; + // (undocumented) + protected abstract _renameFile(path: string, to: string): Observable; + // (undocumented) + protected _validateCreateAction(action: CreateFileAction): Observable; + // (undocumented) + protected _validateDeleteAction(action: DeleteFileAction): Observable; + // (undocumented) + protected abstract _validateFileExists(p: string): Observable; + // (undocumented) + protected _validateOverwriteAction(action: OverwriteFileAction): Observable; + // (undocumented) + protected _validateRenameAction(action: RenameFileAction): Observable; + // (undocumented) + validateSingleAction(action: Action): Observable; +} + +// @public (undocumented) +export interface Sink { + // (undocumented) + commit(tree: Tree_2): Observable; +} + +// @public +export type Source = (context: SchematicContext) => Tree_2 | Observable; + +// @public +export function source(tree: Tree_2): Source; + +// @public (undocumented) +const standardFormats: schema.SchemaFormat[]; + +export { strings } + +// @public (undocumented) +export interface TaskConfiguration { + // (undocumented) + dependencies?: Array; + // (undocumented) + name: string; + // (undocumented) + options?: T; +} + +// @public (undocumented) +export interface TaskConfigurationGenerator { + // (undocumented) + toConfiguration(): TaskConfiguration; +} + +// @public (undocumented) +export type TaskExecutor = (options: T | undefined, context: SchematicContext) => Promise | Observable; + +// @public (undocumented) +export interface TaskExecutorFactory { + // (undocumented) + create(options?: T): Promise | Observable; + // (undocumented) + readonly name: string; +} + +// @public (undocumented) +export interface TaskId { + // (undocumented) + readonly id: number; +} + +// @public (undocumented) +export interface TaskInfo { + // (undocumented) + readonly configuration: TaskConfiguration; + // (undocumented) + readonly context: SchematicContext; + // (undocumented) + readonly id: number; + // (undocumented) + readonly priority: number; +} + +// @public (undocumented) +export class TaskScheduler { + constructor(_context: SchematicContext); + // (undocumented) + finalize(): ReadonlyArray; + // (undocumented) + schedule(taskConfiguration: TaskConfiguration): TaskId; +} + +// @public (undocumented) +export function template(options: T): Rule; + +// @public (undocumented) +export const TEMPLATE_FILENAME_RE: RegExp; + +// @public (undocumented) +export type Tree = Tree_2; + +// @public (undocumented) +export const Tree: TreeConstructor; + +// @public (undocumented) +export interface TreeConstructor { + // (undocumented) + branch(tree: Tree_2): Tree_2; + // (undocumented) + empty(): Tree_2; + // (undocumented) + merge(tree: Tree_2, other: Tree_2, strategy?: MergeStrategy): Tree_2; + // (undocumented) + optimize(tree: Tree_2): Tree_2; + // (undocumented) + partition(tree: Tree_2, predicate: FilePredicate): [Tree_2, Tree_2]; +} + +// @public (undocumented) +export const TreeSymbol: symbol; + +// @public +export interface TypedSchematicContext { + // (undocumented) + addTask(task: TaskConfigurationGenerator, dependencies?: Array): TaskId; + // (undocumented) + readonly debug: boolean; + // (undocumented) + readonly engine: Engine; + // (undocumented) + readonly interactive: boolean; + // (undocumented) + readonly logger: logging.LoggerApi; + // (undocumented) + readonly schematic: Schematic; + // (undocumented) + readonly strategy: MergeStrategy; +} + +// @public (undocumented) +export class UnimplementedException extends BaseException { + constructor(); +} + +// @public (undocumented) +export class UnknownActionException extends BaseException { + constructor(action: Action); +} + +// @public (undocumented) +export class UnknownCollectionException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export class UnknownPipeException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export class UnknownSchematicException extends BaseException { + constructor(name: string, collection: CollectionDescription<{}>); +} + +// @public (undocumented) +export class UnknownTaskDependencyException extends BaseException { + constructor(id: TaskId); +} + +// @public (undocumented) +export class UnknownUrlSourceProtocol extends BaseException { + constructor(url: string); +} + +// @public (undocumented) +export class UnregisteredTaskException extends BaseException { + constructor(name: string, schematic?: SchematicDescription<{}, {}>); +} + +// @public (undocumented) +export class UnsuccessfulWorkflowExecution extends BaseException { + constructor(); +} + +// @public (undocumented) +export interface UpdateRecorder { + // (undocumented) + insertLeft(index: number, content: Buffer | string): UpdateRecorder; + // (undocumented) + insertRight(index: number, content: Buffer | string): UpdateRecorder; + // (undocumented) + remove(index: number, length: number): UpdateRecorder; +} + +// @public (undocumented) +export function url(/service/urlstring: string): Source; + +// @public (undocumented) +export function when(predicate: FilePredicate, operator: FileOperator): FileOperator; + +// @public (undocumented) +interface Workflow { + // (undocumented) + readonly context: Readonly; + // (undocumented) + execute(options: Partial & RequiredWorkflowExecutionContext): Observable; +} + +declare namespace workflow { + export { + BaseWorkflowOptions, + BaseWorkflow, + RequiredWorkflowExecutionContext, + WorkflowExecutionContext, + LifeCycleEvent, + Workflow + } +} +export { workflow } + +// @public (undocumented) +interface WorkflowExecutionContext extends RequiredWorkflowExecutionContext { + // (undocumented) + allowPrivate?: boolean; + // (undocumented) + debug: boolean; + // (undocumented) + logger: logging.Logger; + // (undocumented) + parentContext?: Readonly; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular_devkit/schematics/tasks/index.api.md b/goldens/public-api/angular_devkit/schematics/tasks/index.api.md new file mode 100644 index 000000000000..ad9b7d8d7f0b --- /dev/null +++ b/goldens/public-api/angular_devkit/schematics/tasks/index.api.md @@ -0,0 +1,54 @@ +## API Report File for "@angular-devkit/schematics_tasks" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export class NodePackageInstallTask implements TaskConfigurationGenerator { + constructor(workingDirectory?: string); + constructor(options: NodePackageInstallTaskOptions); + // (undocumented) + allowScripts: boolean; + // (undocumented) + hideOutput: boolean; + // (undocumented) + packageManager?: string; + // (undocumented) + packageName?: string; + // (undocumented) + quiet: boolean; + // (undocumented) + toConfiguration(): TaskConfiguration; + // (undocumented) + workingDirectory?: string; +} + +// @public (undocumented) +export class RepositoryInitializerTask implements TaskConfigurationGenerator { + constructor(workingDirectory?: string | undefined, commitOptions?: CommitOptions | undefined); + // (undocumented) + commitOptions?: CommitOptions | undefined; + // (undocumented) + toConfiguration(): TaskConfiguration; + // (undocumented) + workingDirectory?: string | undefined; +} + +// @public (undocumented) +export class RunSchematicTask implements TaskConfigurationGenerator> { + constructor(s: string, o: T); + constructor(c: string, s: string, o: T); + // (undocumented) + protected _collection: string | null; + // (undocumented) + protected _options: T; + // (undocumented) + protected _schematic: string; + // (undocumented) + toConfiguration(): TaskConfiguration>; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular_devkit/schematics/testing/index.api.md b/goldens/public-api/angular_devkit/schematics/testing/index.api.md new file mode 100644 index 000000000000..abf89972c862 --- /dev/null +++ b/goldens/public-api/angular_devkit/schematics/testing/index.api.md @@ -0,0 +1,43 @@ +## API Report File for "@angular-devkit/schematics_testing" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { JsonValue } from '@angular-devkit/core'; +import { logging } from '@angular-devkit/core'; +import { Observable } from 'rxjs'; +import { Path } from '@angular-devkit/core'; +import { PathFragment } from '@angular-devkit/core'; +import { Url } from 'node:url'; + +// @public (undocumented) +export class SchematicTestRunner { + constructor(_collectionName: string, collectionPath: string); + // (undocumented) + callRule(rule: Rule, tree: Tree_2, parentContext?: Partial): Observable; + // (undocumented) + get engine(): SchematicEngine<{}, {}>; + // (undocumented) + get logger(): logging.Logger; + // (undocumented) + registerCollection(collectionName: string, collectionPath: string): void; + // (undocumented) + runExternalSchematic(collectionName: string, schematicName: string, opts?: SchematicSchemaT, tree?: Tree_2): Promise; + // (undocumented) + runSchematic(schematicName: string, opts?: SchematicSchemaT, tree?: Tree_2): Promise; + // (undocumented) + get tasks(): TaskConfiguration[]; +} + +// @public (undocumented) +export class UnitTestTree extends DelegateTree { + // (undocumented) + get files(): string[]; + // (undocumented) + readContent(path: string): string; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/angular_devkit/schematics/tools/index.api.md b/goldens/public-api/angular_devkit/schematics/tools/index.api.md new file mode 100644 index 000000000000..6a32d88e9ec0 --- /dev/null +++ b/goldens/public-api/angular_devkit/schematics/tools/index.api.md @@ -0,0 +1,280 @@ +## API Report File for "@angular-devkit/schematics_tools" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { BaseException } from '@angular-devkit/core'; +import { JsonObject } from '@angular-devkit/core'; +import { JsonValue } from '@angular-devkit/core'; +import { logging } from '@angular-devkit/core'; +import { Observable } from 'rxjs'; +import { Path } from '@angular-devkit/core'; +import { PathFragment } from '@angular-devkit/core'; +import { schema } from '@angular-devkit/core'; +import { Subject } from 'rxjs'; +import { Url } from 'node:url'; +import { virtualFs } from '@angular-devkit/core'; + +// @public (undocumented) +export class CollectionCannotBeResolvedException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export class CollectionMissingFieldsException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export class CollectionMissingSchematicsMapException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export type ContextTransform = (context: FileSystemSchematicContext) => FileSystemSchematicContext; + +// @public +export class ExportStringRef { + constructor(ref: string, parentPath?: string, inner?: boolean); + // (undocumented) + get module(): string; + // (undocumented) + get path(): string; + // (undocumented) + get ref(): T | undefined; +} + +// @public (undocumented) +export class FactoryCannotBeResolvedException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export type FileSystemCollection = Collection; + +// @public (undocumented) +export type FileSystemCollectionDesc = CollectionDescription; + +// @public (undocumented) +export interface FileSystemCollectionDescription { + // (undocumented) + readonly encapsulation?: boolean; + // (undocumented) + readonly name: string; + // (undocumented) + readonly path: string; + // (undocumented) + readonly schematics: { + [name: string]: FileSystemSchematicDesc; + }; + // (undocumented) + readonly version?: string; +} + +// @public +export type FileSystemEngine = Engine; + +// @public +export class FileSystemEngineHost extends FileSystemEngineHostBase { + constructor(_root: string); + // (undocumented) + createTaskExecutor(name: string): Observable; + // (undocumented) + hasTaskExecutor(name: string): boolean; + // (undocumented) + protected _resolveCollectionPath(name: string): string; + // (undocumented) + protected _resolveReferenceString(refString: string, parentPath: string): { + ref: RuleFactory<{}>; + path: string; + } | null; + // (undocumented) + protected _root: string; + // (undocumented) + protected _transformCollectionDescription(name: string, desc: Partial): FileSystemCollectionDesc; + // (undocumented) + protected _transformSchematicDescription(name: string, _collection: FileSystemCollectionDesc, desc: Partial): FileSystemSchematicDesc; +} + +// @public +export abstract class FileSystemEngineHostBase implements FileSystemEngineHost_2 { + // (undocumented) + createCollectionDescription(name: string, requester?: FileSystemCollectionDesc): FileSystemCollectionDesc; + // (undocumented) + createSchematicDescription(name: string, collection: FileSystemCollectionDesc): FileSystemSchematicDesc | null; + // (undocumented) + createSourceFromUrl(url: Url): Source | null; + // (undocumented) + createTaskExecutor(name: string): Observable; + // (undocumented) + getSchematicRuleFactory(schematic: FileSystemSchematicDesc, _collection: FileSystemCollectionDesc): RuleFactory; + // (undocumented) + hasTaskExecutor(name: string): boolean; + // (undocumented) + listSchematicNames(collection: FileSystemCollectionDesc, includeHidden?: boolean): string[]; + // (undocumented) + registerContextTransform(t: ContextTransform): void; + // (undocumented) + registerOptionsTransform(t: OptionTransform): void; + // (undocumented) + registerTaskExecutor(factory: TaskExecutorFactory, options?: T): void; + // (undocumented) + protected abstract _resolveCollectionPath(name: string, requester?: string): string; + // (undocumented) + protected abstract _resolveReferenceString(name: string, parentPath: string, collectionDescription: FileSystemCollectionDesc): { + ref: RuleFactory<{}>; + path: string; + } | null; + // (undocumented) + protected abstract _transformCollectionDescription(name: string, desc: Partial): FileSystemCollectionDesc; + // (undocumented) + transformContext(context: FileSystemSchematicContext): FileSystemSchematicContext; + // (undocumented) + transformOptions(schematic: FileSystemSchematicDesc, options: OptionT, context?: FileSystemSchematicContext): Observable; + // (undocumented) + protected abstract _transformSchematicDescription(name: string, collection: FileSystemCollectionDesc, desc: Partial): FileSystemSchematicDesc; +} + +// @public (undocumented) +export type FileSystemSchematic = Schematic; + +// @public (undocumented) +export type FileSystemSchematicContext = TypedSchematicContext; + +// @public (undocumented) +export type FileSystemSchematicDesc = SchematicDescription; + +// @public (undocumented) +export interface FileSystemSchematicDescription extends FileSystemSchematicJsonDescription { + // (undocumented) + readonly factoryFn: RuleFactory<{}>; + // (undocumented) + readonly path: string; + // (undocumented) + readonly schemaJson?: JsonObject; +} + +// @public (undocumented) +export interface FileSystemSchematicJsonDescription { + // (undocumented) + readonly aliases?: string[]; + // (undocumented) + readonly collection: FileSystemCollectionDescription; + // (undocumented) + readonly description: string; + // (undocumented) + readonly extends?: string; + // (undocumented) + readonly factory: string; + // (undocumented) + readonly name: string; + // (undocumented) + readonly schema?: string; +} + +// @public (undocumented) +export class InvalidCollectionJsonException extends BaseException { + constructor(_name: string, path: string, jsonException?: Error); +} + +// @public +export class NodeModulesEngineHost extends FileSystemEngineHostBase { + constructor(paths?: string[] | undefined); + // (undocumented) + protected _resolveCollectionPath(name: string, requester?: string): string; + // (undocumented) + protected _resolveReferenceString(refString: string, parentPath: string, collectionDescription?: FileSystemCollectionDesc): { + ref: RuleFactory<{}>; + path: string; + } | null; + // (undocumented) + protected _transformCollectionDescription(name: string, desc: Partial): FileSystemCollectionDesc; + // (undocumented) + protected _transformSchematicDescription(name: string, _collection: FileSystemCollectionDesc, desc: Partial): FileSystemSchematicDesc; +} + +// @public +export class NodeModulesTestEngineHost extends NodeModulesEngineHost { + // (undocumented) + clearTasks(): void; + // (undocumented) + registerCollection(name: string, path: string): void; + // (undocumented) + protected _resolveCollectionPath(name: string, requester?: string): string; + // (undocumented) + get tasks(): TaskConfiguration<{}>[]; + // (undocumented) + transformContext(context: FileSystemSchematicContext): FileSystemSchematicContext; +} + +// @public (undocumented) +export class NodePackageDoesNotSupportSchematics extends BaseException { + constructor(name: string); +} + +// @public +export class NodeWorkflow extends workflow.BaseWorkflow { + constructor(root: string, options: NodeWorkflowOptions); + constructor(host: virtualFs.Host, options: NodeWorkflowOptions & { + root?: Path; + }); + // (undocumented) + get engine(): FileSystemEngine; + // (undocumented) + get engineHost(): NodeModulesEngineHost; +} + +// @public (undocumented) +export interface NodeWorkflowOptions { + // (undocumented) + dryRun?: boolean; + // (undocumented) + engineHostCreator?: (options: NodeWorkflowOptions) => NodeModulesEngineHost; + // (undocumented) + force?: boolean; + // (undocumented) + optionTransforms?: OptionTransform | null, object>[]; + // (undocumented) + packageManager?: string; + // (undocumented) + packageManagerForce?: boolean; + // (undocumented) + packageRegistry?: string; + // (undocumented) + registry?: schema.CoreSchemaRegistry; + // (undocumented) + resolvePaths?: string[]; + // (undocumented) + schemaValidation?: boolean; +} + +// @public (undocumented) +export type OptionTransform = (schematic: FileSystemSchematicDescription, options: T, context?: FileSystemSchematicContext) => Observable | PromiseLike | R; + +// @public (undocumented) +export class SchematicMissingDescriptionException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export class SchematicMissingFactoryException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export class SchematicMissingFieldsException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export class SchematicNameCollisionException extends BaseException { + constructor(name: string); +} + +// @public (undocumented) +export function validateOptionsWithSchema(registry: schema.SchemaRegistry): (schematic: FileSystemSchematicDescription, options: T, context?: FileSystemSchematicContext) => Observable; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/goldens/public-api/manage.js b/goldens/public-api/manage.js new file mode 100644 index 000000000000..569f2a6bd1e3 --- /dev/null +++ b/goldens/public-api/manage.js @@ -0,0 +1,54 @@ +const {exec} = require('shelljs'); +const {Parser: parser} = require('yargs/helpers'); + +// Remove all command line flags from the arguments. +const argv = parser(process.argv.slice(2)); +// The command the user would like to run, either 'accept' or 'test' +const USER_COMMAND = argv._[0]; +// The shell command to query for all Public API guard tests. +const BAZEL_PUBLIC_API_TARGET_QUERY_CMD = + `pnpm -s bazel query --output label 'kind(nodejs_test, ...) intersect attr("tags", "api_guard", ...)'` +// Bazel targets for testing Public API goldens +process.stdout.write('Gathering all Public API targets'); +const ALL_PUBLIC_API_TESTS = exec(BAZEL_PUBLIC_API_TARGET_QUERY_CMD, {silent: true}) + .trim() + .split('\n') + .map(test => test.trim()); +process.stdout.clearLine(); +process.stdout.cursorTo(0); +// Bazel targets for generating Public API goldens +const ALL_PUBLIC_API_ACCEPTS = ALL_PUBLIC_API_TESTS.map(test => `${test}.accept`); + +/** + * Run the provided bazel commands on each provided target individually. + */ +function runBazelCommandOnTargets(command, targets, present) { + for (const target of targets) { + process.stdout.write(`${present}: ${target}`); + const commandResult = exec(`pnpm -s bazel ${command} ${target}`, {silent: true}); + process.stdout.clearLine(); + process.stdout.cursorTo(0); + if (commandResult.code) { + console.error(`Failed ${command}: ${target}`); + console.group(); + console.error(commandResult.stdout || commandResult.stderr); + console.groupEnd(); + } else { + console.log(`Successful ${command}: ${target}`); + } + } +} + +switch (USER_COMMAND) { + case 'accept': + runBazelCommandOnTargets('run', ALL_PUBLIC_API_ACCEPTS, 'Running'); + break; + case 'test': + runBazelCommandOnTargets('test', ALL_PUBLIC_API_TESTS, 'Testing'); + break; + default: + console.warn('Invalid command provided.'); + console.warn(); + console.warn(`Run this script with either "accept" and "test"`); + break; +} diff --git a/goldens/public-api/ngtools/webpack/index.api.md b/goldens/public-api/ngtools/webpack/index.api.md new file mode 100644 index 000000000000..13ddc85cabd8 --- /dev/null +++ b/goldens/public-api/ngtools/webpack/index.api.md @@ -0,0 +1,56 @@ +## API Report File for "@ngtools/webpack" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { Compiler } from 'webpack'; +import type { CompilerOptions } from '@angular/compiler-cli'; +import type { LoaderContext } from 'webpack'; + +// @public (undocumented) +function angularWebpackLoader(this: LoaderContext, content: string, map: string): void; +export default angularWebpackLoader; + +// @public (undocumented) +export const AngularWebpackLoaderPath: string; + +// @public (undocumented) +export class AngularWebpackPlugin { + constructor(options?: Partial); + // (undocumented) + apply(compiler: Compiler): void; + // (undocumented) + get options(): AngularWebpackPluginOptions; +} + +// @public (undocumented) +export interface AngularWebpackPluginOptions { + // (undocumented) + compilerOptions?: CompilerOptions; + // (undocumented) + directTemplateLoading: boolean; + // (undocumented) + emitClassMetadata: boolean; + // (undocumented) + emitNgModuleScope: boolean; + // (undocumented) + emitSetClassDebugInfo?: boolean; + // (undocumented) + fileReplacements: Record; + // (undocumented) + inlineStyleFileExtension?: string; + // (undocumented) + jitMode: boolean; + // (undocumented) + substitutions: Record; + // (undocumented) + tsconfig: string; +} + +// @public (undocumented) +export const imageDomains: Set; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/lib/README.md b/lib/README.md deleted file mode 100644 index bcb80b40bd17..000000000000 --- a/lib/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# `/lib` Folder - -This folder includes bootstrap code for the various tools included in this repository. Also -included is the packages meta-information package in `packages.ts`. This is used to read and -understand all the monorepo information (contained in the `.monorepo.json` file, and `package.json` -files across the repo). - -`bootstrap-local.js` should be included when running files from this repository without compiling -first. It allows for compiling and loading packages in memory directly from the repo. Not only -does the `devkit-admin` scripts use this to include the library, but also all binaries linked -locally (like `schematics` and `ng`), when not using the npm published packages. - -`istanbul-local.js` adds global hooks and information to be able to use code coverage with -`bootstrap-local.js`. Istanbul does not keep the sourcemaps properly in sync if they're available -in the code, which happens locally when transpiling TypeScript. It is currently used by the -`test` script and is included by `bootstrap-local.js` for integration. diff --git a/lib/bootstrap-local.js b/lib/bootstrap-local.js deleted file mode 100644 index fd85505c0bef..000000000000 --- a/lib/bootstrap-local.js +++ /dev/null @@ -1,231 +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 - */ -/* eslint-disable no-console */ -'use strict'; -const debug = require('debug'); -const debugLocal = debug('ng:local'); -const debugBuildEjs = debug('ng:local:build:ejs'); -const debugBuildSchema = debug('ng:local:build:schema'); -const debugBuildTs = debug('ng:local:build:ts'); - -const child_process = require('child_process'); -const fs = require('fs'); -const path = require('path'); -const temp = require('temp'); -const ts = require('typescript'); - -const tmpRoot = temp.mkdirSync('angular-devkit-'); - -debugLocal('starting bootstrap local'); - -// This processes any extended configs -const compilerOptions = ts.getParsedCommandLineOfConfigFile( - path.join(__dirname, '../tsconfig-test.json'), - { }, - ts.sys, -).options; - -let _istanbulRequireHook = null; -if (process.env['CODE_COVERAGE'] || process.argv.indexOf('--code-coverage') !== -1) { - debugLocal('setup code coverage'); - _istanbulRequireHook = require('./istanbul-local').istanbulRequireHook; - // async keyword isn't supported by the Esprima version used by Istanbul version used by us. - // TODO: update istanbul to istanbul-lib-* (see http://istanbul.js.org/) and remove this hack. - compilerOptions.target = 'es2016'; -} - - -// Check if we need to profile this CLI run. -let profiler = null; -if (process.env['DEVKIT_PROFILING']) { - debugLocal('setup profiling'); - try { - profiler = require('v8-profiler-node8'); - } catch (err) { - throw new Error(`Could not require 'v8-profiler-node8'. You must install it separetely with` + - `'npm install v8-profiler-node8 --no-save.\n\nOriginal error:\n\n${err}`); - } - - profiler.startProfiling(); - - function exitHandler(options, _err) { - if (options.cleanup) { - const cpuProfile = profiler.stopProfiling(); - const profileData = JSON.stringify(cpuProfile); - const filePath = path.resolve(process.cwd(), process.env.DEVKIT_PROFILING) + '.cpuprofile'; - - debugLocal('saving profiling data'); - console.log(`Profiling data saved in "${filePath}": ${profileData.length} bytes`); - fs.writeFileSync(filePath, profileData); - } - - if (options.exit) { - process.exit(); - } - } - - process.on('exit', exitHandler.bind(null, { cleanup: true })); - process.on('SIGINT', exitHandler.bind(null, { exit: true })); - process.on('uncaughtException', exitHandler.bind(null, { exit: true })); -} - -if (process.env['DEVKIT_LONG_STACK_TRACE']) { - debugLocal('setup long stack trace'); - Error.stackTraceLimit = Infinity; -} - -global._DevKitIsLocal = true; -global._DevKitRoot = path.resolve(__dirname, '..'); - - -const oldRequireTs = require.extensions['.ts']; -require.extensions['.ts'] = function (m, filename) { - // If we're in node module, either call the old hook or simply compile the - // file without transpilation. We do not touch node_modules/**. - // To account for Yarn workspaces symlinks, we much check the real path. - if (fs.realpathSync(filename).match(/node_modules/)) { - if (oldRequireTs) { - return oldRequireTs(m, filename); - } - return m._compile(fs.readFileSync(filename).toString(), filename); - } - - debugBuildTs(filename); - - // Node requires all require hooks to be sync. - const source = fs.readFileSync(filename).toString(); - - try { - let result = ts.transpile(source, compilerOptions, filename); - - if (_istanbulRequireHook) { - result = _istanbulRequireHook(result, filename); - } - debugBuildTs('done'); - - // Send it to node to execute. - return m._compile(result, filename); - } catch (err) { - console.error('Error while running script "' + filename + '":'); - console.error(err.stack); - throw err; - } -}; - - -require.extensions['.ejs'] = function (m, filename) { - debugBuildEjs(filename); - - const source = fs.readFileSync(filename).toString(); - const template = require('@angular-devkit/core').template; - const result = template(source, { sourceURL: filename, sourceMap: true }); - - debugBuildEjs('done'); - return m._compile(result.source.replace(/return/, 'module.exports.default = '), filename); -}; - -const builtinModules = Object.keys(process.binding('natives')); -const packages = require('./packages').packages; -// If we're running locally, meaning npm linked. This is basically "developer mode". -if (!__dirname.match(new RegExp(`\\${path.sep}node_modules\\${path.sep}`))) { - - // We mock the module loader so that we can fake our packages when running locally. - const Module = require('module'); - const oldLoad = Module._load; - const oldResolve = Module._resolveFilename; - - Module._resolveFilename = function (request, parent) { - let resolved = null; - let exception; - try { - resolved = oldResolve.call(this, request, parent); - } catch (e) { - exception = e; - } - - if (request in packages) { - return packages[request].main; - } else if (builtinModules.includes(request)) { - // It's a native Node module. - return oldResolve.call(this, request, parent); - } else if (resolved && resolved.match(/[\\\/]node_modules[\\\/]/)) { - return resolved; - } else { - const match = Object.keys(packages).find(pkgName => request.startsWith(pkgName + '/')); - if (match) { - const p = path.join(packages[match].root, request.substr(match.length)); - return oldResolve.call(this, p, parent); - } else if (!resolved) { - if (exception) { - throw exception; - } else { - return resolved; - } - } else { - // Because loading `.ts` ends up AFTER `.json` in the require() logic, requiring a file that has both `.json` - // and `.ts` versions will only get the `.json` content (which wouldn't happen if the .ts was compiled to - // JavaScript). We load `.ts` files first here to avoid this conflict. It's hacky, but so is the rest of this - // file. - const maybeTsPath = resolved.endsWith('.json') && resolved.replace(/\.json$/, '.ts'); - if (maybeTsPath && !request.endsWith('.json')) { - // If the file exists, return its path. If it doesn't, run the quicktype runner on it and return the content. - if (fs.existsSync(maybeTsPath)) { - return maybeTsPath; - } else { - debugBuildSchema('%s', resolved); - - // This script has the be synchronous, so we spawnSync instead of, say, requiring the runner and calling - // the method directly. - const tmpJsonSchemaPath = path.join(tmpRoot, maybeTsPath.replace(/[^0-9a-zA-Z.]/g, '_')); - try { - if (!fs.existsSync(tmpJsonSchemaPath)) { - const quicktypeRunnerPath = path.join(__dirname, '../tools/quicktype_runner.js'); - child_process.spawnSync('node', [quicktypeRunnerPath, resolved, tmpJsonSchemaPath]); - } - - debugBuildSchema('done'); - return tmpJsonSchemaPath; - } catch (_) { - // Just return resolvedPath and let Node deals with it. - console.log(_); - process.exit(99); - } - } - } - - return resolved; - } - } - }; -} - - -// Set the resolve hook to allow resolution of packages from a local dev environment. -require('@angular-devkit/core/node/resolve').setResolveHook(function(request, options) { - try { - if (request in packages) { - if (options.resolvePackageJson) { - return path.join(packages[request].root, 'package.json'); - } else { - return packages[request].main; - } - } else { - const match = Object.keys(packages).find(function(pkgName) { - return request.startsWith(pkgName + '/'); - }); - - if (match) { - return path.join(packages[match].root, request.substr(match[0].length)); - } else { - return null; - } - } - } catch (_) { - return null; - } -}); diff --git a/lib/istanbul-local.js b/lib/istanbul-local.js deleted file mode 100644 index f1bdb1ebfe3e..000000000000 --- a/lib/istanbul-local.js +++ /dev/null @@ -1,57 +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 - */ -const { SourceMapConsumer } = require('source-map'); -const Istanbul = require('istanbul'); - -const inlineSourceMapRe = /\/\/# sourceMappingURL=data:application\/json;base64,(\S+)$/; - - -// Use the internal DevKit Hook of the require extension installed by our bootstrapping code to add -// Istanbul (not Constantinople) collection to the code. -const codeMap = new Map(); -exports.codeMap = codeMap; - -exports.istanbulRequireHook = function(code, filename) { - // Skip spec files. - if (filename.match(/_spec(_large)?\.ts$/)) { - return code; - } - const codeFile = codeMap.get(filename); - if (codeFile) { - return codeFile.code; - } - - const instrumenter = new Istanbul.Instrumenter({ - esModules: true, - codeGenerationOptions: { - sourceMap: filename, - sourceMapWithCode: true, - }, - }); - let instrumentedCode = instrumenter.instrumentSync(code, filename); - const match = code.match(inlineSourceMapRe); - - if (match) { - const sourceMapGenerator = instrumenter.sourceMap; - // Fix source maps for exception reporting (since the exceptions happen in the instrumented - // code. - const sourceMapJson = JSON.parse(Buffer.from(match[1], 'base64').toString()); - const consumer = new SourceMapConsumer(sourceMapJson); - sourceMapGenerator.applySourceMap(consumer, filename); - - instrumentedCode = instrumentedCode.replace(inlineSourceMapRe, '') - + '//# sourceMappingURL=data:application/json;base64,' - + Buffer.from(sourceMapGenerator.toString()).toString('base64'); - - // Keep the consumer from the original source map, because the reports from Istanbul (not - // Constantinople) are already mapped against the code. - codeMap.set(filename, { code: instrumentedCode, map: consumer }); - } - - return instrumentedCode; -}; diff --git a/lib/packages.ts b/lib/packages.ts deleted file mode 100644 index 66b3536f484d..000000000000 --- a/lib/packages.ts +++ /dev/null @@ -1,250 +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-next-line:no-implicit-dependencies -import { JsonObject } from '@angular-devkit/core'; -import { execSync } from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as ts from 'typescript'; - -const distRoot = path.join(__dirname, '../dist'); -const { versions: monorepoVersions, packages: monorepoPackages } = require('../.monorepo.json'); - - -export interface PackageInfo { - name: string; - root: string; - bin: { [name: string]: string}; - relative: string; - main: string; - dist: string; - build: string; - tar: string; - private: boolean; - experimental: boolean; - packageJson: JsonObject; - dependencies: string[]; - reverseDependencies: string[]; - - snapshot: boolean; - snapshotRepo: string; - snapshotHash: string; - - version: string; -} -export type PackageMap = { [name: string]: PackageInfo }; - - -function loadPackageJson(p: string) { - const root = require('../package.json'); - const pkg = require(p); - - for (const key of Object.keys(root)) { - switch (key) { - // Keep the following keys from the package.json of the package itself. - case 'bin': - case 'description': - case 'dependencies': - case 'name': - case 'main': - case 'peerDependencies': - case 'optionalDependencies': - case 'typings': - case 'version': - case 'private': - case 'workspaces': - case 'resolutions': - case 'scripts': - continue; - - // Remove the following keys from the package.json. - case 'devDependencies': - delete pkg[key]; - continue; - - // Merge the following keys with the root package.json. - case 'keywords': - const a = pkg[key] || []; - const b = Object.keys( - root[key].concat(a).reduce((acc: {[k: string]: boolean}, curr: string) => { - acc[curr] = true; - - return acc; - }, {})); - pkg[key] = b; - break; - - // Overwrite engines to a common default. - case 'engines': - pkg['engines'] = { - 'node': '>= 10.9.0', - 'npm': '>= 6.2.0', - }; - break; - - // Overwrite the package's key with to root one. - default: - pkg[key] = root[key]; - } - } - - return pkg; -} - - -function _findAllPackageJson(dir: string, exclude: RegExp): string[] { - const result: string[] = []; - fs.readdirSync(dir) - .forEach(fileName => { - const p = path.join(dir, fileName); - - if (exclude.test(p)) { - return; - } else if (/[\/\\]node_modules[\/\\]/.test(p)) { - return; - } else if (fileName == 'package.json') { - result.push(p); - } else if (fs.statSync(p).isDirectory() && fileName != 'node_modules') { - result.push(..._findAllPackageJson(p, exclude)); - } - }); - - return result; -} - - -const tsConfigPath = path.join(__dirname, '../tsconfig.json'); -const tsConfig = ts.readConfigFile(tsConfigPath, ts.sys.readFile); -const pattern = '^(' - + (tsConfig.config.exclude as string[]) - .map(ex => path.join(path.dirname(tsConfigPath), ex)) - .map(ex => '(' - + ex - .replace(/[\-\[\]{}()+?./\\^$|]/g, '\\$&') - .replace(/(\\\\|\\\/)\*\*/g, '((\/|\\\\).+?)?') - .replace(/\*/g, '[^/\\\\]*') - + ')') - .join('|') - + ')($|/|\\\\)'; -const excludeRe = new RegExp(pattern); - -// Find all the package.json that aren't excluded from tsconfig. -const packageJsonPaths = _findAllPackageJson(path.join(__dirname, '..'), excludeRe) - // Remove the root package.json. - .filter(p => p != path.join(__dirname, '../package.json')); - - -function _exec(cmd: string) { - return execSync(cmd).toString().trim(); -} - - -let gitShaCache: string; -function _getSnapshotHash(_pkg: PackageInfo): string { - if (!gitShaCache) { - gitShaCache = _exec('git log --format=%h -n1'); - } - - return gitShaCache; -} - - -let stableVersion = ''; -let experimentalVersion = ''; -function _getVersionFromGit(experimental: boolean): string { - if (stableVersion && experimentalVersion) { - return experimental ? experimentalVersion : stableVersion; - } - - const hasLocalChanges = _exec(`git status --porcelain`) != ''; - const scmVersionTagRaw = _exec(`git describe --match v[0-9].[0-9].[0-9]* --abbrev=7 --tags`) - .slice(1); - stableVersion = scmVersionTagRaw.replace(/-([0-9]+)-g/, '+$1.') - + (hasLocalChanges ? '.with-local-changes' : ''); - - experimentalVersion = `0.${stableVersion.replace(/^(\d+)\.(\d+)/, (_, major, minor) => { - return '' + (parseInt(major, 10) * 100 + parseInt(minor, 10)); - })}`; - - return experimental ? experimentalVersion : stableVersion; -} - - -// All the supported packages. Go through the packages directory and create a map of -// name => PackageInfo. This map is partial as it lacks some information that requires the -// map itself to finish building. -export const packages: PackageMap = - packageJsonPaths - .map(pkgPath => ({ root: path.dirname(pkgPath) })) - .reduce((packages: PackageMap, pkg) => { - const pkgRoot = pkg.root; - const packageJson = loadPackageJson(path.join(pkgRoot, 'package.json')); - const name = packageJson['name']; - if (!name) { - // Only build the entry if there's a package name. - return packages; - } - if (!(name in monorepoPackages)) { - throw new Error( - `Package ${name} found in ${JSON.stringify(pkg.root)}, not found in .monorepo.json.`, - ); - } - - const bin: {[name: string]: string} = {}; - Object.keys(packageJson['bin'] || {}).forEach(binName => { - let p = path.resolve(pkg.root, packageJson['bin'][binName]); - if (!fs.existsSync(p)) { - p = p.replace(/\.js$/, '.ts'); - } - bin[binName] = p; - }); - - const experimental = !!packageJson.private || !!packageJson.experimental; - - packages[name] = { - build: path.join(distRoot, pkgRoot.substr(path.dirname(__dirname).length)), - dist: path.join(distRoot, name), - root: pkgRoot, - relative: path.relative(path.dirname(__dirname), pkgRoot), - main: path.resolve(pkgRoot, 'src/index.ts'), - private: !!packageJson.private, - experimental, - // yarn doesn't take kindly to @ in tgz filenames - // https://github.com/yarnpkg/yarn/issues/6339 - tar: path.join(distRoot, name.replace(/\/|@/g, '_') + '.tgz'), - bin, - name, - packageJson, - - snapshot: !!monorepoPackages[name].snapshotRepo, - snapshotRepo: monorepoPackages[name].snapshotRepo, - get snapshotHash() { - return _getSnapshotHash(this); - }, - - dependencies: [], - reverseDependencies: [], - get version() { - return _getVersionFromGit(experimental); - }, - }; - - return packages; - }, {}); - - -// Update with dependencies. -for (const pkgName of Object.keys(packages)) { - const pkg = packages[pkgName]; - const pkgJson = require(path.join(pkg.root, 'package.json')); - pkg.dependencies = Object.keys(packages).filter(name => { - return name in (pkgJson.dependencies || {}) - || name in (pkgJson.devDependencies || {}); - }); - pkg.dependencies.forEach(depName => packages[depName].reverseDependencies.push(pkgName)); -} diff --git a/modules/testing/builder/BUILD.bazel b/modules/testing/builder/BUILD.bazel new file mode 100644 index 000000000000..9814ad27811d --- /dev/null +++ b/modules/testing/builder/BUILD.bazel @@ -0,0 +1,50 @@ +load("@npm//:defs.bzl", "npm_link_all_packages") +load("//tools:defaults.bzl", "jasmine_test", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +npm_link_all_packages() + +ts_project( + name = "builder", + testonly = True, + srcs = glob( + include = [ + "src/**/*.ts", + ], + exclude = [ + "src/**/*_spec.ts", + ], + ), + data = [ + # Needed at runtime by some builder tests relying on SSR being + # resolvable in the test project. + ":node_modules/@angular/ssr", + ":node_modules/vitest", + ] + glob(["projects/**/*"]), + deps = [ + ":node_modules/@angular-devkit/architect", + ":node_modules/@angular-devkit/core", + ":node_modules/rxjs", + "//:node_modules/@types/node", + ], +) + +ts_project( + name = "unit_test_lib", + testonly = True, + srcs = glob( + include = [ + "src/**/*_spec.ts", + ], + ), + deps = [ + ":builder", + ":node_modules/@angular-devkit/architect", + ], +) + +jasmine_test( + name = "unit_test", + data = [":unit_test_lib"], +) diff --git a/modules/testing/builder/package.json b/modules/testing/builder/package.json new file mode 100644 index 000000000000..50e0cbc98b7b --- /dev/null +++ b/modules/testing/builder/package.json @@ -0,0 +1,10 @@ +{ + "devDependencies": { + "@angular-devkit/core": "workspace:*", + "@angular-devkit/architect": "workspace:*", + "@angular/ssr": "workspace:*", + "@angular-devkit/build-angular": "workspace:*", + "rxjs": "7.8.2", + "vitest": "3.1.2" + } +} diff --git a/tests/angular_devkit/build_angular/hello-world-app-ivy/.editorconfig b/modules/testing/builder/projects/hello-world-app/.editorconfig similarity index 100% rename from tests/angular_devkit/build_angular/hello-world-app-ivy/.editorconfig rename to modules/testing/builder/projects/hello-world-app/.editorconfig diff --git a/tests/angular_devkit/build_angular/hello-world-app-ivy/.gitignore b/modules/testing/builder/projects/hello-world-app/.gitignore similarity index 100% rename from tests/angular_devkit/build_angular/hello-world-app-ivy/.gitignore rename to modules/testing/builder/projects/hello-world-app/.gitignore diff --git a/modules/testing/builder/projects/hello-world-app/README.md b/modules/testing/builder/projects/hello-world-app/README.md new file mode 100644 index 000000000000..3c578babc961 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/README.md @@ -0,0 +1,27 @@ +# HelloWorldApp + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.7.0-beta.1. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate --help` to see all the available schematics you can generate. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/main/README.md). diff --git a/modules/testing/builder/projects/hello-world-app/angular.json b/modules/testing/builder/projects/hello-world-app/angular.json new file mode 100644 index 000000000000..95607701be8f --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/angular.json @@ -0,0 +1,191 @@ +{ + "$schema": "../../../../packages/angular_devkit/core/src/workspace/workspace-schema.json", + "version": 1, + "newProjectRoot": "./projects", + "cli": { + "cache": { + "enabled": false + } + }, + "schematics": {}, + "projects": { + "app": { + "root": "src", + "sourceRoot": "src", + "projectType": "application", + "prefix": "app", + "schematics": {}, + "targets": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "progress": false, + "sourceMap": false, + "aot": false, + "vendorChunk": true, + "buildOptimizer": false, + "optimization": false, + "extractLicenses": false, + "namedChunks": true, + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + }, + "inline-critical-css": { + "optimization": { + "styles": { + "minify": true, + "inlineCritical": true + }, + "scripts": true, + "fonts": true + } + }, + "sw": { + "ngswConfigPath": "src/ngsw-config.json", + "serviceWorker": true + } + } + }, + "server": { + "builder": "@angular-devkit/build-angular:server", + "options": { + "outputPath": "dist-server", + "main": "src/main.server.ts", + "tsConfig": "src/tsconfig.server.json", + "progress": false, + "sourceMap": true, + "optimization": false, + "extractLicenses": false + }, + "configurations": { + "production": { + "outputHashing": "media", + "sourceMap": false, + "optimization": true + } + } + }, + "app-shell": { + "builder": "@angular-devkit/build-angular:app-shell", + "options": { + "browserTarget": "app:build", + "serverTarget": "app:server" + }, + "configurations": { + "production": { + "browserTarget": "app:build:production" + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "buildTarget": "app:build", + "watch": false + }, + "configurations": { + "production": { + "buildTarget": "app:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "app:build", + "progress": false, + "outputPath": "src" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "karma.conf.js", + "browsers": "ChromeHeadlessCI", + "progress": false, + "watch": false, + "styles": [ + { + "input": "src/styles.css" + } + ], + "scripts": [], + "assets": [ + "src/favicon.ico", + "src/assets" + ] + } + }, + "serve-ssr": { + "builder": "@angular-devkit/build-angular:ssr-dev-server", + "options": { + "browserTarget": "app:build", + "serverTarget": "app:server", + "watch": false, + "progress": false + }, + "configurations": { + "production": { + "browserTarget": "app:build:production", + "serverTarget": "app:server:production" + } + } + }, + "prerender": { + "builder": "@angular-devkit/build-angular:prerender", + "options": { + "browserTarget": "app:build", + "serverTarget": "app:server" + }, + "configurations": { + "production": { + "browserTarget": "app:build:production", + "serverTarget": "app:server:production" + } + } + } + } + }, + "app-e2e": { + "root": "e2e", + "projectType": "application", + "targets": { + "e2e": { + "builder": "@angular-devkit/build-angular:private-protractor", + "options": { + "protractorConfig": "protractor.conf.js", + "devServerTarget": "app:serve", + "webdriverUpdate": false + } + } + } + } + } +} diff --git a/modules/testing/builder/projects/hello-world-app/e2e/app.e2e-spec.ts b/modules/testing/builder/projects/hello-world-app/e2e/app.e2e-spec.ts new file mode 100644 index 000000000000..862e95302ce3 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/e2e/app.e2e-spec.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 { AppPage } from './app.po'; + +describe('hello-world-app App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', async () => { + page.navigateTo(); + expect(await page.getTitleText()).toEqual('Welcome to app!'); + }); +}); diff --git a/modules/testing/builder/projects/hello-world-app/e2e/app.po.ts b/modules/testing/builder/projects/hello-world-app/e2e/app.po.ts new file mode 100644 index 000000000000..7cb6bc3fb743 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/e2e/app.po.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo(): Promise { + return browser.get(browser.baseUrl) as Promise; + } + + getTitleText(): Promise { + return element(by.css('app-root h1')).getText() as Promise; + } +} diff --git a/modules/testing/builder/projects/hello-world-app/e2e/tsconfig.e2e.json b/modules/testing/builder/projects/hello-world-app/e2e/tsconfig.e2e.json new file mode 100644 index 000000000000..a82df00eef37 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/e2e/tsconfig.e2e.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/e2e", + "module": "commonjs", + "target": "es2018", + "types": [ + "jasmine", + "node" + ] + } +} diff --git a/modules/testing/builder/projects/hello-world-app/karma.conf.js b/modules/testing/builder/projects/hello-world-app/karma.conf.js new file mode 100644 index 000000000000..7ad5e58e8efc --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/karma.conf.js @@ -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 + */ + +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +const path = require('path'); +process.env.CHROME_BIN = require('puppeteer').executablePath(); + +module.exports = function(config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma'), + ], + client: { + clearContext: false, // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: path.join(__dirname, './coverage'), + subdir: '.', + reporters: [ + {type: 'lcov'}, + ], + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + customLaunchers: { + ChromeHeadlessCI: { + base: 'ChromeHeadless', + flags: [ + '--disable-gpu', + ...(process.env.CHROME_NO_SANDBOX === '1' ? ['--no-sandbox'] : []), + ], + }, + }, + singleRun: false, + }); +}; diff --git a/modules/testing/builder/projects/hello-world-app/protractor.conf.js b/modules/testing/builder/projects/hello-world-app/protractor.conf.js new file mode 100644 index 000000000000..313f7ac7c53b --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/protractor.conf.js @@ -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 + */ + +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); +const { resolve } = require('path'); + +exports.config = { + allScriptsTimeout: 11000, + specs: ['./e2e/**/*.e2e-spec.ts'], + capabilities: { + browserName: 'chrome', + chromeOptions: { + args: ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'], + binary: require('puppeteer').executablePath(), + }, + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {}, + }, + onPrepare() { + require('ts-node').register({ + project: resolve(__dirname, './e2e/tsconfig.e2e.json'), + }); + jasmine.getEnv().addReporter(new SpecReporter({ + spec: { + displayStacktrace: StacktraceOption.PRETTY + } + })); + }, +}; diff --git a/tests/angular_devkit/build_angular/hello-world-app-ivy/src/app/app.component.css b/modules/testing/builder/projects/hello-world-app/src/app/app.component.css similarity index 100% rename from tests/angular_devkit/build_angular/hello-world-app-ivy/src/app/app.component.css rename to modules/testing/builder/projects/hello-world-app/src/app/app.component.css diff --git a/modules/testing/builder/projects/hello-world-app/src/app/app.component.html b/modules/testing/builder/projects/hello-world-app/src/app/app.component.html new file mode 100644 index 000000000000..fe14e2bc127c --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/app/app.component.html @@ -0,0 +1,22 @@ + +
+

+ Welcome to {{ title }}! +

+ Angular Logo +
+

Here are some links to help you start:

+
+ +

i18n test

+ diff --git a/modules/testing/builder/projects/hello-world-app/src/app/app.component.spec.ts b/modules/testing/builder/projects/hello-world-app/src/app/app.component.spec.ts new file mode 100644 index 000000000000..37e8de10ba72 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/app/app.component.spec.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 { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; +describe('AppComponent', () => { + beforeEach(() => TestBed.configureTestingModule({ + declarations: [AppComponent], + })); + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + }); + it(`should have as title 'app'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('app'); + }); + it('should render title in a h1 tag', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); + }); +}); diff --git a/modules/testing/builder/projects/hello-world-app/src/app/app.component.ts b/modules/testing/builder/projects/hello-world-app/src/app/app.component.ts new file mode 100644 index 000000000000..93a041e8aec7 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/app/app.component.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + standalone: false, + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'], +}) +export class AppComponent { + title = 'app'; +} diff --git a/modules/testing/builder/projects/hello-world-app/src/app/app.module.server.ts b/modules/testing/builder/projects/hello-world-app/src/app/app.module.server.ts new file mode 100644 index 000000000000..76fd51151562 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/app/app.module.server.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 { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; + +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + imports: [ + AppModule, + ServerModule, + ], + bootstrap: [AppComponent], +}) +export class AppServerModule {} diff --git a/modules/testing/builder/projects/hello-world-app/src/app/app.module.ts b/modules/testing/builder/projects/hello-world-app/src/app/app.module.ts new file mode 100644 index 000000000000..d8060d5ac4c9 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/app/app.module.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; + + +import { AppComponent } from './app.component'; + + +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/tests/angular_devkit/build_angular/hello-world-app-ivy/src/assets/.gitkeep b/modules/testing/builder/projects/hello-world-app/src/assets/.gitkeep similarity index 100% rename from tests/angular_devkit/build_angular/hello-world-app-ivy/src/assets/.gitkeep rename to modules/testing/builder/projects/hello-world-app/src/assets/.gitkeep diff --git a/tests/angular_devkit/build_angular/hello-world-app-ivy/src/favicon.ico b/modules/testing/builder/projects/hello-world-app/src/favicon.ico similarity index 100% rename from tests/angular_devkit/build_angular/hello-world-app-ivy/src/favicon.ico rename to modules/testing/builder/projects/hello-world-app/src/favicon.ico diff --git a/tests/angular_devkit/build_angular/hello-world-app-ivy/src/index.html b/modules/testing/builder/projects/hello-world-app/src/index.html similarity index 100% rename from tests/angular_devkit/build_angular/hello-world-app-ivy/src/index.html rename to modules/testing/builder/projects/hello-world-app/src/index.html diff --git a/tests/angular_devkit/build_angular/hello-world-app-ivy/src/src/locale/messages.xlf b/modules/testing/builder/projects/hello-world-app/src/locale/messages.xlf similarity index 100% rename from tests/angular_devkit/build_angular/hello-world-app-ivy/src/src/locale/messages.xlf rename to modules/testing/builder/projects/hello-world-app/src/locale/messages.xlf diff --git a/modules/testing/builder/projects/hello-world-app/src/main.server.ts b/modules/testing/builder/projects/hello-world-app/src/main.server.ts new file mode 100644 index 000000000000..0c26cf60e980 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/main.server.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { AppServerModule } from './app/app.module.server'; +export default AppServerModule; +export { AppServerModule }; diff --git a/modules/testing/builder/projects/hello-world-app/src/main.ts b/modules/testing/builder/projects/hello-world-app/src/main.ts new file mode 100644 index 000000000000..d975b48d7808 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/main.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { platformBrowser } from '@angular/platform-browser'; + +import { AppModule } from './app/app.module'; + +platformBrowser() + .bootstrapModule(AppModule) + .catch(err => console.log(err)); diff --git a/modules/testing/builder/projects/hello-world-app/src/ngsw-config.json b/modules/testing/builder/projects/hello-world-app/src/ngsw-config.json new file mode 100644 index 000000000000..789b03bdf7ce --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/ngsw-config.json @@ -0,0 +1,30 @@ +{ + "$schema": "../node_modules/@angular/service-worker/config/schema.json", + "index": "/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": [ + "/favicon.ico", + "/index.html", + "/manifest.webmanifest", + "/*.css", + "/*.js" + ] + } + }, + { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": [ + "/assets/**", + "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" + ] + } + } + ] +} diff --git a/modules/testing/builder/projects/hello-world-app/src/polyfills.ts b/modules/testing/builder/projects/hello-world-app/src/polyfills.ts new file mode 100644 index 000000000000..eb77e392ee4e --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/polyfills.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.dev/reference/versions#browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags.ts'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/tests/angular_devkit/build_angular/hello-world-app-ivy/src/spectrum.png b/modules/testing/builder/projects/hello-world-app/src/spectrum.png similarity index 100% rename from tests/angular_devkit/build_angular/hello-world-app-ivy/src/spectrum.png rename to modules/testing/builder/projects/hello-world-app/src/spectrum.png diff --git a/tests/angular_devkit/build_angular/hello-world-app-ivy/src/styles.css b/modules/testing/builder/projects/hello-world-app/src/styles.css similarity index 100% rename from tests/angular_devkit/build_angular/hello-world-app-ivy/src/styles.css rename to modules/testing/builder/projects/hello-world-app/src/styles.css diff --git a/modules/testing/builder/projects/hello-world-app/src/tsconfig.app.json b/modules/testing/builder/projects/hello-world-app/src/tsconfig.app.json new file mode 100644 index 000000000000..a76843b689fb --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "types": ["@angular/localize"] + }, + "files": [ + "main.ts", + "polyfills.ts" + ], + "include": [ + "**/*.d.ts" + ] +} diff --git a/modules/testing/builder/projects/hello-world-app/src/tsconfig.server.json b/modules/testing/builder/projects/hello-world-app/src/tsconfig.server.json new file mode 100644 index 000000000000..9a492c781eb1 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/tsconfig.server.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../dist-server", + "baseUrl": "./", + "types": ["@angular/localize", "node"] + }, + "files": [ + "main.server.ts" + ] +} diff --git a/modules/testing/builder/projects/hello-world-app/src/tsconfig.spec.json b/modules/testing/builder/projects/hello-world-app/src/tsconfig.spec.json new file mode 100644 index 000000000000..a42459f71db5 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "types": [ + "jasmine", + "@angular/localize" + ] + }, + "files": [ + "polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/modules/testing/builder/projects/hello-world-app/src/typings.d.ts b/modules/testing/builder/projects/hello-world-app/src/typings.d.ts new file mode 100644 index 000000000000..161056781dc0 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/src/typings.d.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/* SystemJS module definition */ +declare var module: NodeModule; +interface NodeModule { + id: string; +} diff --git a/modules/testing/builder/projects/hello-world-app/tsconfig.json b/modules/testing/builder/projects/hello-world-app/tsconfig.json new file mode 100644 index 000000000000..8019279a7006 --- /dev/null +++ b/modules/testing/builder/projects/hello-world-app/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "target": "es2022", + "module": "es2022", + "lib": [ + "es2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableIvy": true, + "disableTypeScriptVersionCheck": true, + "strictTemplates": true + } +} diff --git a/modules/testing/builder/src/builder-harness.ts b/modules/testing/builder/src/builder-harness.ts new file mode 100644 index 000000000000..ecee882739d8 --- /dev/null +++ b/modules/testing/builder/src/builder-harness.ts @@ -0,0 +1,527 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 import/no-extraneous-dependencies */ + +import { + BuilderContext, + BuilderHandlerFn, + BuilderInfo, + BuilderOutput, + BuilderOutputLike, + BuilderProgressReport, + BuilderRun, + ScheduleOptions, + Target, + fromAsyncIterable, + isBuilderOutput, +} from '@angular-devkit/architect'; +import { WorkspaceHost } from '@angular-devkit/architect/node'; +import { TestProjectHost } from '@angular-devkit/architect/testing'; +import { getSystemPath, json, logging } from '@angular-devkit/core'; +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import fs from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { + Observable, + Subject, + catchError, + finalize, + firstValueFrom, + lastValueFrom, + map, + mergeMap, + from as observableFrom, + of as observableOf, + shareReplay, +} from 'rxjs'; +import { BuilderWatcherFactory, WatcherNotifier } from './file-watching'; + +export interface BuilderHarnessExecutionResult { + result?: T; + error?: Error; + logs: readonly logging.LogEntry[]; +} + +export interface BuilderHarnessExecutionOptions { + configuration: string; + outputLogsOnFailure: boolean; + outputLogsOnException: boolean; + useNativeFileWatching: boolean; + signal: AbortSignal; + additionalExecuteArguments: unknown[]; +} + +interface BuilderHandlerFnWithVarArgs extends BuilderHandlerFn { + (input: T, context: BuilderContext, ...args: unknown[]): BuilderOutputLike; +} + +/** + * The default set of fields provided to all builders executed via the BuilderHarness. + * `root` and `sourceRoot` are required for most Angular builders to function. + * `cli.cache.enabled` set to false provides improved test isolation guarantees by disabling + * the Webpack caching. + */ +const DEFAULT_PROJECT_METADATA = { + root: '.', + sourceRoot: 'src', + cli: { + cache: { + enabled: false, + }, + }, +}; + +export class BuilderHarness { + private readonly builderInfo: BuilderInfo; + private schemaRegistry = new json.schema.CoreSchemaRegistry(); + private projectName = 'test'; + private projectMetadata: Record = DEFAULT_PROJECT_METADATA; + private targetName?: string; + private options = new Map(); + private builderTargets = new Map< + string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + { handler: BuilderHandlerFn; info: BuilderInfo; options: json.JsonObject } + >(); + private watcherNotifier?: WatcherNotifier; + + constructor( + private readonly builderHandler: BuilderHandlerFn, + private readonly host: TestProjectHost, + builderInfo?: Partial, + ) { + // Generate default pseudo builder info for test purposes + this.builderInfo = { + builderName: builderHandler.name, + description: '', + optionSchema: true, + ...builderInfo, + }; + + if (builderInfo?.builderName?.startsWith('@angular/build:')) { + this.schemaRegistry.addPostTransform(json.schema.transforms.addUndefinedObjectDefaults); + } else { + this.schemaRegistry.addPostTransform(json.schema.transforms.addUndefinedDefaults); + } + } + + private resolvePath(path: string): string { + return join(getSystemPath(this.host.root()), path); + } + + useProject(name: string, metadata: Record = {}): this { + if (!name) { + throw new Error('Project name cannot be an empty string.'); + } + + this.projectName = name; + this.projectMetadata = metadata; + + return this; + } + + useTarget(name: string, baseOptions: T): this { + if (!name) { + throw new Error('Target name cannot be an empty string.'); + } + + this.targetName = name; + this.options.set(null, baseOptions); + + return this; + } + + withConfiguration(configuration: string, options: T): this { + this.options.set(configuration, options); + + return this; + } + + withBuilderTarget( + target: string, + handler: BuilderHandlerFn, + options?: O, + info?: Partial, + ): this { + this.builderTargets.set(target, { + handler, + options: options || {}, + info: { builderName: handler.name, description: '', optionSchema: true, ...info }, + }); + + return this; + } + + execute( + options: Partial = {}, + ): Observable { + const { + configuration, + outputLogsOnException = true, + outputLogsOnFailure = true, + useNativeFileWatching = false, + } = options; + + const targetOptions = { + ...this.options.get(null), + ...((configuration && this.options.get(configuration)) ?? {}), + }; + + if (!useNativeFileWatching) { + if (this.watcherNotifier) { + throw new Error('Only one harness execution at a time is supported.'); + } + this.watcherNotifier = new WatcherNotifier(); + } + + const contextHost: ContextHost = { + findBuilderByTarget: async (project, target) => { + this.validateProjectName(project); + if (target === this.targetName) { + return { + info: this.builderInfo, + handler: this.builderHandler as BuilderHandlerFn, + }; + } + + const builderTarget = this.builderTargets.get(target); + if (builderTarget) { + return { info: builderTarget.info, handler: builderTarget.handler }; + } + + throw new Error('Project target does not exist.'); + }, + async getBuilderName(project, target) { + return (await this.findBuilderByTarget(project, target)).info.builderName; + }, + getMetadata: async (project) => { + this.validateProjectName(project); + + return this.projectMetadata as json.JsonObject; + }, + getOptions: async (project, target, configuration) => { + this.validateProjectName(project); + if (target === this.targetName) { + return (this.options.get(configuration ?? null) ?? {}) as json.JsonObject; + } else if (configuration !== undefined) { + // Harness builder targets currently do not support configurations + return {}; + } else { + return this.builderTargets.get(target)?.options || {}; + } + }, + hasTarget: async (project, target) => { + this.validateProjectName(project); + + return this.targetName === target || this.builderTargets.has(target); + }, + getDefaultConfigurationName: async (_project, _target) => { + return undefined; + }, + validate: async (options, builderName) => { + let schema; + if (builderName === this.builderInfo.builderName) { + schema = this.builderInfo.optionSchema; + } else { + for (const [, value] of this.builderTargets) { + if (value.info.builderName === builderName) { + schema = value.info.optionSchema; + break; + } + } + } + + const validator = await this.schemaRegistry.compile(schema ?? true); + const { data } = await validator(options); + + return data as json.JsonObject; + }, + }; + const context = new HarnessBuilderContext( + this.builderInfo, + this.resolvePath('.'), + contextHost, + options.signal, + useNativeFileWatching ? undefined : this.watcherNotifier, + ); + if (this.targetName !== undefined) { + context.target = { + project: this.projectName, + target: this.targetName, + configuration: configuration as string, + }; + } + + const logs: logging.LogEntry[] = []; + context.logger.subscribe((e) => logs.push(e)); + + return observableFrom(this.schemaRegistry.compile(this.builderInfo.optionSchema)).pipe( + mergeMap((validator) => validator(targetOptions)), + map((validationResult) => validationResult.data), + mergeMap((data) => + convertBuilderOutputToObservable( + (this.builderHandler as BuilderHandlerFnWithVarArgs)( + data as T & json.JsonObject, + context, + ...(options.additionalExecuteArguments ?? []), + ), + ), + ), + map((buildResult) => ({ result: buildResult, error: undefined })), + catchError((error) => { + if (outputLogsOnException) { + // eslint-disable-next-line no-console + console.error(logs.map((entry) => entry.message).join('\n')); + // eslint-disable-next-line no-console + console.error(error); + } + + return observableOf({ result: undefined, error }); + }), + map(({ result, error }) => { + if (outputLogsOnFailure && result?.success === false && logs.length > 0) { + // eslint-disable-next-line no-console + console.error(logs.map((entry) => entry.message).join('\n')); + } + + // Capture current logs and clear for next + const currentLogs = logs.slice(); + logs.length = 0; + + return { result, error, logs: currentLogs }; + }), + finalize(() => { + this.watcherNotifier = undefined; + + for (const teardown of context.teardowns) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + teardown(); + } + }), + ); + } + + async executeOnce( + options?: Partial, + ): Promise { + // Return the first result + return firstValueFrom(this.execute(options)); + } + + async appendToFile(path: string, content: string): Promise { + await this.writeFile(path, this.readFile(path).concat(content)); + } + + async writeFile(path: string, content: string | Buffer): Promise { + const fullPath = this.resolvePath(path); + + await fs.mkdir(dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, content, 'utf-8'); + + this.watcherNotifier?.notify([{ path: fullPath, type: 'modified' }]); + } + + async writeFiles(files: Record): Promise { + const watchEvents = this.watcherNotifier + ? ([] as { path: string; type: 'modified' | 'deleted' }[]) + : undefined; + + for (const [path, content] of Object.entries(files)) { + const fullPath = this.resolvePath(path); + + await fs.mkdir(dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, content, 'utf-8'); + + watchEvents?.push({ path: fullPath, type: 'modified' }); + } + + if (watchEvents) { + this.watcherNotifier?.notify(watchEvents); + } + } + + async removeFile(path: string): Promise { + const fullPath = this.resolvePath(path); + + await fs.unlink(fullPath); + + this.watcherNotifier?.notify([{ path: fullPath, type: 'deleted' }]); + } + + async modifyFile( + path: string, + modifier: (content: string) => string | Promise, + ): Promise { + const content = this.readFile(path); + await this.writeFile(path, await modifier(content)); + } + + hasFile(path: string): boolean { + const fullPath = this.resolvePath(path); + + return existsSync(fullPath); + } + + hasDirectory(path: string): boolean { + const fullPath = this.resolvePath(path); + + return statSync(fullPath, { throwIfNoEntry: false })?.isDirectory() ?? false; + } + + hasFileMatch(directory: string, pattern: RegExp): boolean { + const fullPath = this.resolvePath(directory); + + return readdirSync(fullPath).some((name) => pattern.test(name)); + } + + readFile(path: string): string { + const fullPath = this.resolvePath(path); + + return readFileSync(fullPath, 'utf-8'); + } + + private validateProjectName(name: string): void { + if (name !== this.projectName) { + throw new Error(`Project "${name}" does not exist.`); + } + } +} + +interface ContextHost extends WorkspaceHost { + findBuilderByTarget( + project: string, + target: string, + ): Promise<{ info: BuilderInfo; handler: BuilderHandlerFn }>; + validate(options: json.JsonObject, builderName: string): Promise; +} + +class HarnessBuilderContext implements BuilderContext { + id = Math.trunc(Math.random() * 1000000); + logger = new logging.Logger(`builder-harness-${this.id}`); + workspaceRoot: string; + currentDirectory: string; + target?: Target; + + teardowns: (() => Promise | void)[] = []; + + constructor( + public builder: BuilderInfo, + basePath: string, + private readonly contextHost: ContextHost, + public readonly signal: AbortSignal | undefined, + public readonly watcherFactory: BuilderWatcherFactory | undefined, + ) { + this.workspaceRoot = this.currentDirectory = basePath; + } + + addTeardown(teardown: () => Promise | void): void { + this.teardowns.push(teardown); + } + + async getBuilderNameForTarget(target: Target): Promise { + return this.contextHost.getBuilderName(target.project, target.target); + } + + async getProjectMetadata(targetOrName: Target | string): Promise { + const project = typeof targetOrName === 'string' ? targetOrName : targetOrName.project; + + return this.contextHost.getMetadata(project); + } + + async getTargetOptions(target: Target): Promise { + return this.contextHost.getOptions(target.project, target.target, target.configuration); + } + + // Unused by builders in this package + async scheduleBuilder( + builderName: string, + options?: json.JsonObject, + scheduleOptions?: ScheduleOptions, + ): Promise { + throw new Error('Not Implemented.'); + } + + async scheduleTarget( + target: Target, + overrides?: json.JsonObject, + scheduleOptions?: ScheduleOptions, + ): Promise { + const { info, handler } = await this.contextHost.findBuilderByTarget( + target.project, + target.target, + ); + const targetOptions = await this.validateOptions( + { + ...(await this.getTargetOptions(target)), + ...overrides, + }, + info.builderName, + ); + + const context = new HarnessBuilderContext( + info, + this.workspaceRoot, + this.contextHost, + this.signal, + this.watcherFactory, + ); + context.target = target; + context.logger = scheduleOptions?.logger || this.logger.createChild(''); + + const progressSubject = new Subject(); + const output = convertBuilderOutputToObservable(handler(targetOptions, context)); + + const run: BuilderRun = { + id: context.id, + info, + progress: progressSubject.asObservable(), + async stop() { + for (const teardown of context.teardowns) { + await teardown(); + } + progressSubject.complete(); + }, + output: output.pipe(shareReplay()), + get result() { + return firstValueFrom(this.output); + }, + get lastOutput() { + return lastValueFrom(this.output); + }, + }; + + return run; + } + + async validateOptions( + options: json.JsonObject, + builderName: string, + ): Promise { + return this.contextHost.validate(options, builderName) as unknown as T; + } + + // Unused report methods + reportRunning(): void {} + reportStatus(): void {} + reportProgress(): void {} +} + +function isAsyncIterable(obj: unknown): obj is AsyncIterable { + return !!obj && typeof (obj as AsyncIterable)[Symbol.asyncIterator] === 'function'; +} + +function convertBuilderOutputToObservable(output: BuilderOutputLike): Observable { + if (isBuilderOutput(output)) { + return observableOf(output); + } else if (isAsyncIterable(output)) { + return fromAsyncIterable(output); + } else { + return observableFrom(output); + } +} diff --git a/modules/testing/builder/src/builder-harness_spec.ts b/modules/testing/builder/src/builder-harness_spec.ts new file mode 100644 index 000000000000..860764f567f8 --- /dev/null +++ b/modules/testing/builder/src/builder-harness_spec.ts @@ -0,0 +1,137 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 import/no-extraneous-dependencies */ + +import { TestProjectHost } from '@angular-devkit/architect/testing'; +import { BuilderHarness } from './builder-harness'; + +describe('BuilderHarness', () => { + let mockHost: TestProjectHost; + + beforeEach(() => { + mockHost = jasmine.createSpyObj('TestProjectHost', ['root']); + (mockHost.root as jasmine.Spy).and.returnValue('.'); + }); + + it('uses the provided builder handler', async () => { + const mockHandler = jasmine.createSpy().and.returnValue({ success: true }); + + const harness = new BuilderHarness(mockHandler, mockHost); + + await harness.executeOnce(); + + expect(mockHandler).toHaveBeenCalled(); + }); + + it('provides the builder output result when executing', async () => { + const mockHandler = jasmine.createSpy().and.returnValue({ success: false, property: 'value' }); + + const harness = new BuilderHarness(mockHandler, mockHost); + const { result } = await harness.executeOnce(); + + expect(result).toBeDefined(); + expect(result?.success).toBeFalse(); + expect(result?.property).toBe('value'); + }); + + it('does not show builder logs on console when a builder succeeds', async () => { + const consoleErrorMock = spyOn(console, 'error'); + + const harness = new BuilderHarness(async (_, context) => { + context.logger.warn('TEST WARNING'); + + return { success: true }; + }, mockHost); + + const { result } = await harness.executeOnce(); + + expect(result).toBeDefined(); + expect(result?.success).toBeTrue(); + + expect(consoleErrorMock).not.toHaveBeenCalledWith(jasmine.stringMatching('TEST WARNING')); + }); + + it('shows builder logs on console when a builder fails', async () => { + const consoleErrorMock = spyOn(console, 'error'); + + const harness = new BuilderHarness(async (_, context) => { + context.logger.warn('TEST WARNING'); + + return { success: false }; + }, mockHost); + + const { result } = await harness.executeOnce(); + + expect(result).toBeDefined(); + expect(result?.success).toBeFalse(); + + expect(consoleErrorMock).toHaveBeenCalledWith(jasmine.stringMatching('TEST WARNING')); + }); + + it('does not show builder logs on console when a builder fails and outputLogsOnFailure: false', async () => { + const consoleErrorMock = spyOn(console, 'error'); + + const harness = new BuilderHarness(async (_, context) => { + context.logger.warn('TEST WARNING'); + + return { success: false }; + }, mockHost); + + const { result } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result).toBeDefined(); + expect(result?.success).toBeFalse(); + + expect(consoleErrorMock).not.toHaveBeenCalledWith(jasmine.stringMatching('TEST WARNING')); + }); + + it('provides and logs the builder output exception when builder throws', async () => { + const mockHandler = jasmine.createSpy().and.throwError(new Error('Builder Error')); + const consoleErrorMock = spyOn(console, 'error'); + + const harness = new BuilderHarness(mockHandler, mockHost); + const { result, error } = await harness.executeOnce(); + + expect(result).toBeUndefined(); + expect(error).toEqual(jasmine.objectContaining({ message: 'Builder Error' })); + expect(consoleErrorMock).toHaveBeenCalledWith(jasmine.stringMatching('Builder Error')); + }); + + it('does not log exception with outputLogsOnException false when builder throws', async () => { + const mockHandler = jasmine.createSpy().and.throwError(new Error('Builder Error')); + const consoleErrorMock = spyOn(console, 'error'); + + const harness = new BuilderHarness(mockHandler, mockHost); + const { result, error } = await harness.executeOnce({ outputLogsOnException: false }); + + expect(result).toBeUndefined(); + expect(error).toEqual(jasmine.objectContaining({ message: 'Builder Error' })); + expect(consoleErrorMock).not.toHaveBeenCalledWith(jasmine.stringMatching('Builder Error')); + }); + + it('supports executing a target from within a builder', async () => { + const mockHandler = jasmine.createSpy().and.returnValue({ success: true }); + + const harness = new BuilderHarness(async (_, context) => { + const run = await context.scheduleTarget({ project: 'test', target: 'another' }); + expect(await run.result).toEqual(jasmine.objectContaining({ success: true })); + await run.stop(); + + return { success: true }; + }, mockHost); + harness.withBuilderTarget('another', mockHandler); + + const { result } = await harness.executeOnce(); + + expect(result).toBeDefined(); + expect(result?.success).toBeTrue(); + + expect(mockHandler).toHaveBeenCalled(); + }); +}); diff --git a/modules/testing/builder/src/dev_prod_mode.ts b/modules/testing/builder/src/dev_prod_mode.ts new file mode 100644 index 000000000000..520de82d771b --- /dev/null +++ b/modules/testing/builder/src/dev_prod_mode.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 { BuilderHarness } from './builder-harness'; + +export const GOOD_TARGET = './src/good.js'; +export const BAD_TARGET = './src/bad.js'; + +/** Setup project for use of conditional imports. */ +export async function setupConditionImport(harness: BuilderHarness) { + // Files that can be used as targets for the conditional import. + await harness.writeFile('src/good.ts', `export const VALUE = 'good-value';`); + await harness.writeFile('src/bad.ts', `export const VALUE = 'bad-value';`); + await harness.writeFile('src/wrong.ts', `export const VALUE = 1;`); + + // Simple application file that accesses conditional code. + await harness.writeFile( + 'src/main.ts', + `import {VALUE} from '#target'; +console.log(VALUE); +console.log(VALUE.length); +export default 42 as any; +`, + ); + + // Ensure that good/bad can be resolved from tsconfig. + const tsconfig = JSON.parse(harness.readFile('src/tsconfig.app.json')) as TypeScriptConfig; + tsconfig.compilerOptions.moduleResolution = 'bundler'; + tsconfig.files.push('good.ts', 'bad.ts', 'wrong.ts'); + await harness.writeFile('src/tsconfig.app.json', JSON.stringify(tsconfig)); +} + +/** Update package.json with the given mapping for #target. */ +export async function setTargetMapping(harness: BuilderHarness, mapping: unknown) { + await harness.writeFile( + 'package.json', + JSON.stringify({ + name: 'ng-test-app', + imports: { + '#target': mapping, + }, + }), + ); +} + +interface TypeScriptConfig { + compilerOptions: { + moduleResolution: string; + }; + files: string[]; +} diff --git a/modules/testing/builder/src/file-watching.ts b/modules/testing/builder/src/file-watching.ts new file mode 100644 index 000000000000..6d10706d6919 --- /dev/null +++ b/modules/testing/builder/src/file-watching.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 + */ + +type BuilderWatcherCallback = ( + events: Array<{ path: string; type: 'created' | 'modified' | 'deleted'; time?: number }>, +) => void; + +interface BuilderWatcherFactory { + watch( + files: Iterable, + directories: Iterable, + callback: BuilderWatcherCallback, + ): { close(): void }; +} + +class WatcherDescriptor { + constructor( + readonly files: ReadonlySet, + readonly directories: ReadonlySet, + readonly callback: BuilderWatcherCallback, + ) {} + + shouldNotify(path: string): boolean { + return true; + } +} + +export class WatcherNotifier implements BuilderWatcherFactory { + private readonly descriptors = new Set(); + + notify(events: Iterable<{ path: string; type: 'modified' | 'deleted' }>): void { + for (const descriptor of this.descriptors) { + for (const { path } of events) { + if (descriptor.shouldNotify(path)) { + descriptor.callback([...events]); + break; + } + } + } + } + + watch( + files: Iterable, + directories: Iterable, + callback: BuilderWatcherCallback, + ): { close(): void } { + const descriptor = new WatcherDescriptor(new Set(files), new Set(directories), callback); + this.descriptors.add(descriptor); + + return { close: () => this.descriptors.delete(descriptor) }; + } +} + +export type { BuilderWatcherFactory }; diff --git a/modules/testing/builder/src/index.ts b/modules/testing/builder/src/index.ts new file mode 100644 index 000000000000..8f4687f9fdca --- /dev/null +++ b/modules/testing/builder/src/index.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export { + BuilderHarness, + type BuilderHarnessExecutionOptions, + type BuilderHarnessExecutionResult, +} from './builder-harness'; +export { + type HarnessFileMatchers, + JasmineBuilderHarness, + describeBuilder, +} from './jasmine-helpers'; +export * from './test-utils'; diff --git a/modules/testing/builder/src/jasmine-helpers.ts b/modules/testing/builder/src/jasmine-helpers.ts new file mode 100644 index 000000000000..b204d507bab8 --- /dev/null +++ b/modules/testing/builder/src/jasmine-helpers.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 { BuilderHandlerFn } from '@angular-devkit/architect'; +import { json } from '@angular-devkit/core'; +import { readFileSync } from 'node:fs'; +import { concatMap, count, firstValueFrom, take, timeout } from 'rxjs'; +import { BuilderHarness, BuilderHarnessExecutionResult } from './builder-harness'; +import { host } from './test-utils'; + +/** + * Maximum time for single build/rebuild + * This accounts for CI variability. + */ +export const BUILD_TIMEOUT = 25_000; + +const optionSchemaCache = new Map(); + +export function describeBuilder( + builderHandler: BuilderHandlerFn, + options: { name?: string; schemaPath: string }, + specDefinitions: (harness: JasmineBuilderHarness) => void, +): void { + let optionSchema = optionSchemaCache.get(options.schemaPath); + if (optionSchema === undefined) { + optionSchema = JSON.parse(readFileSync(options.schemaPath, 'utf8')) as json.schema.JsonSchema; + optionSchemaCache.set(options.schemaPath, optionSchema); + } + const harness = new JasmineBuilderHarness(builderHandler, host, { + builderName: options.name, + optionSchema, + }); + + describe(options.name || builderHandler.name, () => { + beforeEach(() => host.initialize().toPromise()); + + afterEach(() => host.restore().toPromise()); + + specDefinitions(harness); + }); +} + +export class JasmineBuilderHarness extends BuilderHarness { + expectFile(path: string): HarnessFileMatchers { + return expectFile(path, this); + } + expectDirectory(path: string): HarnessDirectoryMatchers { + return expectDirectory(path, this); + } + + async executeWithCases( + cases: (( + executionResult: BuilderHarnessExecutionResult, + index: number, + ) => void | Promise)[], + ): Promise { + const executionCount = await firstValueFrom( + this.execute().pipe( + timeout(BUILD_TIMEOUT), + concatMap(async (result, index) => await cases[index](result, index)), + take(cases.length), + count(), + ), + ); + + expect(executionCount).toBe(cases.length); + } +} + +export interface HarnessFileMatchers { + toExist(): boolean; + toNotExist(): boolean; + readonly content: jasmine.ArrayLikeMatchers; + readonly size: jasmine.Matchers; +} + +export interface HarnessDirectoryMatchers { + toExist(): boolean; + toNotExist(): boolean; +} + +/** + * Add a Jasmine expectation filter to an expectation that always fails with a message. + * @param base The base expectation (`expect(...)`) to use. + * @param message The message to provide in the expectation failure. + */ +function createFailureExpectation(base: T, message: string): T { + // Needed typings are not included in the Jasmine types + const expectation = base as T & { + expector: { + addFilter(filter: { + selectComparisonFunc(): () => { pass: boolean; message: string }; + }): typeof expectation.expector; + }; + }; + expectation.expector = expectation.expector.addFilter({ + selectComparisonFunc() { + return () => ({ + pass: false, + message, + }); + }, + }); + + return expectation; +} + +export function expectFile(path: string, harness: BuilderHarness): HarnessFileMatchers { + return { + toExist() { + const exists = harness.hasFile(path); + expect(exists).toBe(true, 'Expected file to exist: ' + path); + + return exists; + }, + toNotExist() { + const exists = harness.hasFile(path); + expect(exists).toBe(false, 'Expected file to not exist: ' + path); + + return !exists; + }, + get content() { + try { + return expect(harness.readFile(path)).withContext(`With file content for '${path}'`); + } catch (e) { + if ((e as NodeJS.ErrnoException).code !== 'ENOENT') { + throw e; + } + + // File does not exist so always fail the expectation + return createFailureExpectation( + expect(''), + `Expected file content but file does not exist: '${path}'`, + ); + } + }, + get size() { + try { + return expect(Buffer.byteLength(harness.readFile(path))).withContext( + `With file size for '${path}'`, + ); + } catch (e) { + if ((e as NodeJS.ErrnoException).code !== 'ENOENT') { + throw e; + } + + // File does not exist so always fail the expectation + return createFailureExpectation( + expect(0), + `Expected file size but file does not exist: '${path}'`, + ); + } + }, + }; +} + +export function expectDirectory( + path: string, + harness: BuilderHarness, +): HarnessDirectoryMatchers { + return { + toExist() { + const exists = harness.hasDirectory(path); + expect(exists).toBe(true, 'Expected directory to exist: ' + path); + + return exists; + }, + toNotExist() { + const exists = harness.hasDirectory(path); + expect(exists).toBe(false, 'Expected directory to not exist: ' + path); + + return !exists; + }, + }; +} diff --git a/modules/testing/builder/src/test-utils.ts b/modules/testing/builder/src/test-utils.ts new file mode 100644 index 000000000000..126af073b726 --- /dev/null +++ b/modules/testing/builder/src/test-utils.ts @@ -0,0 +1,184 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 import/no-extraneous-dependencies */ + +import { Architect, BuilderOutput, ScheduleOptions, Target } from '@angular-devkit/architect'; +import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; +import { TestProjectHost, TestingArchitectHost } from '@angular-devkit/architect/testing'; +import { + Path, + getSystemPath, + join, + json, + normalize, + schema, + virtualFs, + workspaces, +} from '@angular-devkit/core'; +import path from 'node:path'; +import { firstValueFrom } from 'rxjs'; + +// Default timeout for large specs is 2.5 minutes. +jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000; + +export const workspaceRoot = join(normalize(__dirname), `../projects/hello-world-app/`); +export const host = new TestProjectHost(workspaceRoot); +export const outputPath: Path = normalize('dist'); + +export const browserTargetSpec = { project: 'app', target: 'build' }; +export const devServerTargetSpec = { project: 'app', target: 'serve' }; +export const extractI18nTargetSpec = { project: 'app', target: 'extract-i18n' }; +export const karmaTargetSpec = { project: 'app', target: 'test' }; +export const tslintTargetSpec = { project: 'app', target: 'lint' }; +export const protractorTargetSpec = { project: 'app-e2e', target: 'e2e' }; + +export async function createArchitect(workspaceRoot: Path) { + const registry = new schema.CoreSchemaRegistry(); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); + const workspaceSysPath = getSystemPath(workspaceRoot); + + // The download path is relative (set from Starlark), so before potentially + // changing directories, or executing inside a temporary directory, ensure + // the path is absolute. + if (process.env['PUPPETEER_DOWNLOAD_PATH']) { + process.env.PUPPETEER_DOWNLOAD_PATH = path.resolve(process.env['PUPPETEER_DOWNLOAD_PATH']); + } + + const { workspace } = await workspaces.readWorkspace( + workspaceSysPath, + workspaces.createWorkspaceHost(host), + ); + const architectHost = new TestingArchitectHost( + workspaceSysPath, + workspaceSysPath, + new WorkspaceNodeModulesArchitectHost(workspace, workspaceSysPath), + ); + const architect = new Architect(architectHost, registry); + + return { + workspace, + architectHost, + architect, + }; +} + +export interface BrowserBuildOutput { + output: BuilderOutput; + files: { [file: string]: Promise }; +} + +export async function browserBuild( + architect: Architect, + host: virtualFs.Host, + target: Target, + overrides?: json.JsonObject, + scheduleOptions?: ScheduleOptions, +): Promise { + const run = await architect.scheduleTarget(target, overrides, scheduleOptions); + const output = (await run.result) as BuilderOutput & { outputs: { path: string }[] }; + expect(output.success).toBe(true); + + if (!output.success) { + await run.stop(); + + return { + output, + files: {}, + }; + } + + const [{ path }] = output.outputs; + expect(path).toBeTruthy(); + const outputPath = normalize(path); + + const fileNames = await firstValueFrom(host.list(outputPath)); + const files = fileNames.reduce((acc: { [name: string]: Promise }, path) => { + let cache: Promise | null = null; + Object.defineProperty(acc, path, { + enumerable: true, + get() { + if (cache) { + return cache; + } + if (!fileNames.includes(path)) { + return Promise.reject('No file named ' + path); + } + cache = firstValueFrom(host.read(join(outputPath, path))).then((content) => + virtualFs.fileBufferToString(content), + ); + + return cache; + }, + }); + + return acc; + }, {}); + + await run.stop(); + + return { + output, + files, + }; +} + +export const lazyModuleFiles: { [path: string]: string } = { + 'src/app/lazy/lazy-routing.module.ts': ` + import { NgModule } from '@angular/core'; + import { Routes, RouterModule } from '@angular/router'; + + const routes: Routes = []; + + @NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] + }) + export class LazyRoutingModule { } + `, + 'src/app/lazy/lazy.module.ts': ` + import { NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + + import { LazyRoutingModule } from './lazy-routing.module'; + + @NgModule({ + imports: [ + CommonModule, + LazyRoutingModule + ], + declarations: [] + }) + export class LazyModule { } + `, +}; + +export const lazyModuleFnImport: { [path: string]: string } = { + 'src/app/app.module.ts': ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + + import { AppComponent } from './app.component'; + import { RouterModule } from '@angular/router'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + RouterModule.forRoot([ + { path: 'lazy', loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule) } + ]) + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } +`, +}; diff --git a/package.json b/package.json index a2c1dfa85e88..62d79122b507 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,8 @@ { "name": "@angular/devkit-repo", - "version": "0.0.0", + "version": "20.1.0-next.0", "private": true, "description": "Software Development Kit for Angular", - "bin": { - "architect": "./bin/architect", - "benchmark": "./bin/benchmark", - "build-optimizer": "./bin/build-optimizer", - "devkit-admin": "./bin/devkit-admin", - "ng": "./bin/ng", - "schematics": "./bin/schematics" - }, "keywords": [ "angular", "Angular CLI", @@ -19,32 +11,33 @@ "Angular DevKit" ], "scripts": { - "admin": "node ./bin/devkit-admin", - "bazel:format": "find . -type f \\( -name \"*.bzl\" -or -name WORKSPACE -or -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" | xargs buildifier -v --warnings=attr-cfg,attr-license,attr-non-empty,attr-output-default,attr-single-file,constant-glob,ctx-args,depset-iteration,depset-union,dict-concatenation,duplicated-name,filetype,git-repository,http-archive,integer-division,load,load-on-top,native-build,native-package,output-group,package-name,package-on-top,positional-args,redefined-variable,repository-name,same-origin-load,string-iteration,unused-variable", - "bazel:lint": "yarn bazel:format --lint=warn", - "bazel:lint-fix": "yarn bazel:format --lint=fix", - "bazel:test": "bazel test //...", - "build": "npm run admin -- build", - "build-tsc": "tsc -p tsconfig.json", - "fix": "npm run admin -- lint --fix", - "lint": "npm run admin -- lint", - "templates": "node ./bin/devkit-admin templates", - "test": "node ./bin/devkit-admin test", - "test-large": "node ./bin/devkit-admin test --large --spec-reporter", - "test-cli-e2e": "node ./tests/legacy-cli/run_e2e", - "test:watch": "nodemon --watch packages -e ts ./bin/devkit-admin test", - "validate": "node ./bin/devkit-admin validate", - "validate-commits": "./bin/devkit-admin validate-commits", - "preinstall": "node ./tools/yarn/check-yarn.js", - "webdriver-update": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.45" + "admin": "node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only ./scripts/devkit-admin.mjs", + "bazel": "bazelisk", + "test": "bazel test //packages/...", + "build": "pnpm -s admin build", + "lint": "eslint --cache --max-warnings=0 \"**/*.@(ts|mts|cts)\"", + "templates": "pnpm -s admin templates", + "validate": "pnpm -s admin validate", + "postinstall": "pnpm -s webdriver-update && husky", + "//webdriver-update-README": "ChromeDriver version must match Puppeteer Chromium version, see https://github.com/GoogleChrome/puppeteer/releases http://chromedriver.chromium.org/downloads", + "webdriver-update": "webdriver-manager update --standalone false --gecko false --versions.chrome 106.0.5249.21", + "public-api:check": "node goldens/public-api/manage.js test", + "ng-dev": "node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only node_modules/@angular/ng-dev/bundles/cli.mjs", + "public-api:update": "node goldens/public-api/manage.js accept", + "ts-circular-deps": "pnpm -s ng-dev ts-circular-deps --config ./scripts/circular-deps-test.conf.mjs", + "check-tooling-setup": "tsc --project .ng-dev/tsconfig.json", + "diff-release-package": "node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only scripts/diff-release-package.mts" }, "repository": { "type": "git", "url": "https://github.com/angular/angular-cli.git" }, + "packageManager": "pnpm@9.15.6", "engines": { - "node": ">=10.9.0 <13.0.0", - "yarn": ">=1.9.0 <2.0.0" + "node": "^20.11.1 || >=22.11.0", + "npm": "Please use pnpm instead of NPM to install dependencies", + "yarn": "Please use pnpm instead of Yarn to install dependencies", + "pnpm": "^9.15.6" }, "author": "Angular Authors", "license": "MIT", @@ -52,98 +45,134 @@ "url": "https://github.com/angular/angular-cli/issues" }, "homepage": "https://github.com/angular/angular-cli", - "workspaces": { - "packages": [ - "packages/angular/*", - "packages/angular_devkit/*", - "packages/ngtools/*", - "packages/schematics/*" - ], - "nohoist": [ - "@angular/compiler-cli" - ] - }, - "dependencies": { - "@types/debug": "^4.1.2", - "@types/node-fetch": "^2.1.6", + "devDependencies": { + "@angular/animations": "20.0.0-next.9", + "@angular/cdk": "20.0.0-next.10", + "@angular/common": "20.0.0-next.9", + "@angular/compiler": "20.0.0-next.9", + "@angular/compiler-cli": "20.0.0-next.9", + "@angular/core": "20.0.0-next.9", + "@angular/forms": "20.0.0-next.9", + "@angular/localize": "20.0.0-next.9", + "@angular/material": "20.0.0-next.10", + "@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#1a12d97905f4af88ccc0b582864907729d23e23e", + "@angular/platform-browser": "20.0.0-next.9", + "@angular/platform-server": "20.0.0-next.9", + "@angular/router": "20.0.0-next.9", + "@angular/service-worker": "20.0.0-next.9", + "@bazel/bazelisk": "1.26.0", + "@bazel/buildifier": "8.2.0", + "@eslint/compat": "1.2.9", + "@eslint/eslintrc": "3.3.1", + "@eslint/js": "9.25.1", + "@rollup/plugin-alias": "^5.1.1", + "@rollup/plugin-commonjs": "^28.0.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "16.0.1", + "@stylistic/eslint-plugin": "^4.0.0", + "@types/babel__core": "7.20.5", + "@types/babel__generator": "^7.6.8", + "@types/browser-sync": "^2.27.0", + "@types/express": "~5.0.1", + "@types/http-proxy": "^1.17.4", + "@types/ini": "^4.0.0", + "@types/jasmine": "~5.1.0", + "@types/jasmine-reporters": "^2", + "@types/karma": "^6.3.0", + "@types/less": "^3.0.3", + "@types/loader-utils": "^2.0.0", + "@types/lodash": "^4.17.0", + "@types/node": "^20.17.19", + "@types/npm-package-arg": "^6.1.0", + "@types/pacote": "^11.1.3", + "@types/picomatch": "^4.0.0", "@types/progress": "^2.0.3", - "@types/universal-analytics": "^0.4.2", - "@types/uuid": "^3.4.4", - "debug": "^4.1.1", - "glob": "^7.0.3", - "node-fetch": "^2.2.0", - "prettier": "^1.16.4", - "puppeteer": "1.12.2", - "quicktype-core": "^6.0.15", - "temp": "^0.9.0", - "tslint": "^5.15.0", - "typescript": "3.4.5" + "@types/resolve": "^1.17.1", + "@types/semver": "^7.3.12", + "@types/shelljs": "^0.8.11", + "@types/watchpack": "^2.4.4", + "@types/yargs": "^17.0.20", + "@types/yargs-parser": "^21.0.0", + "@types/yarnpkg__lockfile": "^1.1.5", + "@typescript-eslint/eslint-plugin": "8.31.1", + "@typescript-eslint/parser": "8.31.1", + "ajv": "8.17.1", + "ansi-colors": "4.1.3", + "beasties": "0.3.3", + "buffer": "6.0.3", + "esbuild": "0.25.3", + "esbuild-wasm": "0.25.3", + "eslint": "9.25.1", + "eslint-config-prettier": "10.1.2", + "eslint-plugin-header": "3.1.1", + "eslint-plugin-import": "2.31.0", + "express": "5.1.0", + "fast-glob": "3.3.3", + "globals": "16.0.0", + "http-proxy": "^1.18.1", + "http-proxy-middleware": "3.0.5", + "husky": "9.1.7", + "jasmine": "~5.7.0", + "jasmine-core": "~5.7.0", + "jasmine-reporters": "^2.5.2", + "jasmine-spec-reporter": "~7.0.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "karma-source-map-support": "1.4.0", + "listr2": "8.3.2", + "lodash": "^4.17.21", + "npm": "^11.0.0", + "magic-string": "0.30.17", + "rollup-plugin-dts": "6.2.1", + "rollup-plugin-sourcemaps2": "0.5.1", + "prettier": "^3.0.0", + "protractor": "~7.0.0", + "puppeteer": "18.2.1", + "quicktype-core": "23.1.3", + "rollup": "4.40.1", + "rollup-license-plugin": "~3.0.1", + "semver": "7.7.1", + "shelljs": "^0.9.0", + "source-map-support": "0.5.21", + "tar": "^7.0.0", + "ts-node": "^10.9.1", + "tslib": "2.8.1", + "typescript": "5.8.3", + "undici": "7.8.0", + "unenv": "^1.10.0", + "verdaccio": "6.1.2", + "verdaccio-auth-memory": "^10.0.0", + "yargs-parser": "21.1.1", + "zone.js": "^0.15.0" }, - "devDependencies": { - "@angular/compiler": "~8.1.0-next.1", - "@angular/compiler-cli": "~8.1.0-next.1", - "@bazel/bazel": "0.24.1", - "@bazel/buildifier": "^0.22.0", - "@bazel/jasmine": "~0.26.0", - "@bazel/karma": "~0.26.0", - "@bazel/typescript": "~0.26.0", - "@types/browserslist": "^4.4.0", - "@types/caniuse-lite": "^1.0.0", - "@types/copy-webpack-plugin": "^4.4.1", - "@types/express": "^4.16.0", - "@types/glob": "^7.0.0", - "@types/inquirer": "^0.0.44", - "@types/istanbul": "^0.4.30", - "@types/jasmine": "^3.3.8", - "@types/karma": "^3.0.2", - "@types/loader-utils": "^1.1.3", - "@types/minimist": "^1.2.0", - "@types/node": "10.9.4", - "@types/request": "^2.47.1", - "@types/semver": "^6.0.0", - "@types/source-map": "0.5.2", - "@types/webpack": "^4.4.11", - "@types/webpack-dev-server": "^3.1.0", - "@types/webpack-sources": "^0.1.5", - "@yarnpkg/lockfile": "1.1.0", - "ajv": "6.10.0", - "ansi-colors": "3.2.4", - "common-tags": "^1.8.0", - "conventional-changelog": "^1.1.0", - "conventional-commits-parser": "^3.0.0", - "gh-got": "^8.0.1", - "git-raw-commits": "^2.0.0", - "husky": "^1.3.1", - "istanbul": "^0.4.5", - "jasmine": "^3.3.1", - "jasmine-spec-reporter": "^4.2.1", - "karma": "~4.1.0", - "karma-jasmine": "^2.0.1", - "karma-jasmine-html-reporter": "^1.4.0", - "license-checker": "^20.1.0", - "minimatch": "^3.0.4", - "minimist": "^1.2.0", - "npm-registry-client": "8.6.0", - "pacote": "^9.2.3", - "pidtree": "^0.3.0", - "pidusage": "^2.0.17", - "rxjs": "~6.4.0", - "semver": "6.2.0", - "source-map": "^0.5.6", - "source-map-support": "^0.5.0", - "spdx-satisfies": "^4.0.0", - "tar": "^4.4.4", - "through2": "^2.0.3", - "tree-kill": "^1.2.0", - "ts-api-guardian": "0.4.4", - "ts-node": "^5.0.0", - "tslint-no-circular-imports": "^0.7.0", - "tslint-sonarts": "1.9.0", - "verdaccio": "4.0.3" + "dependenciesMeta": { + "esbuild": { + "built": true + }, + "puppeteer": { + "built": true + } }, - "husky": { - "hooks": { - "pre-push": "node ./bin/devkit-admin hooks/pre-push" + "pnpm": { + "onlyBuiltDependencies": [ + "puppeteer", + "webdriver-manager" + ], + "overrides": { + "@angular/build": "workspace:*" + }, + "packageExtensions": { + "grpc-gcp": { + "peerDependencies": { + "protobufjs": "*" + } + } } + }, + "resolutions": { + "typescript": "5.8.3" } } diff --git a/packages/README.md b/packages/README.md index 82160f26a4f6..aab7eadc1c92 100644 --- a/packages/README.md +++ b/packages/README.md @@ -2,8 +2,7 @@ This folder is the root of all defined packages in this repository. -Packages that are marked as `private: true` will not be published to NPM. These are limited to the -`_` subfolder. +Packages that are marked as `private: true` will not be published to NPM. -This folder includes a directory for every scope in NPM, without the `@` sign. Then one folder +This folder includes a directory for every scope in NPM, without the `@` sign. Then one folder per package, which contains the `package.json`. diff --git a/packages/_/benchmark/package.json b/packages/_/benchmark/package.json deleted file mode 100644 index ba691f860a1a..000000000000 --- a/packages/_/benchmark/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@_/benchmark", - "version": "0.0.0", - "description": "CLI tool for Angular", - "main": "src/index.js", - "typings": "src/index.d.ts", - "scripts": { - "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" - }, - "private": true -} diff --git a/packages/_/benchmark/src/benchmark.ts b/packages/_/benchmark/src/benchmark.ts deleted file mode 100644 index 7c22abe0136f..000000000000 --- a/packages/_/benchmark/src/benchmark.ts +++ /dev/null @@ -1,86 +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 - */ -declare const global: { - benchmarkReporter: { - reportBenchmark: Function, - }, -}; - - -const kNanosecondsPerSeconds = 1e9; -const kBenchmarkIterationMaxCount = 10000; -const kBenchmarkTimeoutInMsec = 5000; -const kWarmupIterationCount = 100; -const kTopMetricCount = 5; - - -function _run(fn: (i: number) => void, collector: number[]) { - const timeout = Date.now(); - // Gather the first 5 seconds runs, or kMaxNumberOfIterations runs whichever comes first - // (soft timeout). - for (let i = 0; - i < kBenchmarkIterationMaxCount && (Date.now() - timeout) < kBenchmarkTimeoutInMsec; - i++) { - // Start time. - const start = process.hrtime(); - fn(i); - // Get the stop difference time. - const diff = process.hrtime(start); - - // Add to metrics. - collector.push(diff[0] * kNanosecondsPerSeconds + diff[1]); - } -} - - -function _stats(metrics: number[]) { - metrics.sort((a, b) => a - b); - - const count = metrics.length; - const middle = count / 2; - const mean = Number.isInteger(middle) - ? metrics[middle] : ((metrics[middle - 0.5] + metrics[middle + 0.5]) / 2); - const total = metrics.reduce((acc, curr) => acc + curr, 0); - const average = total / count; - - return { - count: count, - fastest: metrics.slice(0, kTopMetricCount), - slowest: metrics.reverse().slice(0, kTopMetricCount), - mean, - average, - }; -} - - -export function benchmark(name: string, fn: (i: number) => void, base?: (i: number) => void) { - it(name + ' (time in nanoseconds)', (done) => { - process.nextTick(() => { - for (let i = 0; i < kWarmupIterationCount; i++) { - // Warm it up. - fn(i); - } - - const reporter = global.benchmarkReporter; - const metrics: number[] = []; - const baseMetrics: number[] = []; - - _run(fn, metrics); - if (base) { - _run(base, baseMetrics); - } - - reporter.reportBenchmark({ - ..._stats(metrics), - base: base ? _stats(baseMetrics) : null, - }); - - done(); - }); - }); -} diff --git a/packages/_/benchmark/src/index.ts b/packages/_/benchmark/src/index.ts deleted file mode 100644 index df56b2754b0b..000000000000 --- a/packages/_/benchmark/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -export * from './benchmark'; diff --git a/packages/_/devkit/collection.json b/packages/_/devkit/collection.json deleted file mode 100644 index 0353b644307c..000000000000 --- a/packages/_/devkit/collection.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "schematics": { - "package": { - "factory": "./package/factory", - "schema": "./package/schema.json", - "description": "Create an empty schematic project or add a blank schematic to the current project." - } - } -} diff --git a/packages/_/devkit/package.json b/packages/_/devkit/package.json deleted file mode 100644 index 23b4a671ac7e..000000000000 --- a/packages/_/devkit/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "devkit", - "version": "0.0.0", - "description": "Schematics specific to DevKit (used internally, not released)", - "scripts": { - "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" - }, - "schematics": "./collection.json", - "private": true, - "dependencies": { - "@angular-devkit/core": "0.0.0", - "@angular-devkit/schematics": "0.0.0" - } -} diff --git a/packages/_/devkit/package/factory.ts b/packages/_/devkit/package/factory.ts deleted file mode 100644 index 9068a3d4ac24..000000000000 --- a/packages/_/devkit/package/factory.ts +++ /dev/null @@ -1,102 +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, JsonValue, parseJsonAst } from '@angular-devkit/core'; -import { - Rule, - Tree, - UpdateRecorder, - apply, - chain, - mergeWith, - template, - url, -} from '@angular-devkit/schematics'; -import { Schema } from './schema'; - - -function appendPropertyInAstObject( - recorder: UpdateRecorder, - node: JsonAstObject, - propertyName: string, - value: JsonValue, - indent = 4, -) { - const indentStr = '\n' + new Array(indent + 1).join(' '); - - if (node.properties.length > 0) { - // Insert comma. - const last = node.properties[node.properties.length - 1]; - recorder.insertRight(last.start.offset + last.text.replace(/\s+$/, '').length, ','); - } - - recorder.insertLeft( - node.end.offset - 1, - ' ' - + `"${propertyName}": ${JSON.stringify(value, null, 2).replace(/\n/g, indentStr)}` - + indentStr.slice(0, -2), - ); -} - -function addPackageToMonorepo(options: Schema, path: string): Rule { - return (tree: Tree) => { - const collectionJsonContent = tree.read('/.monorepo.json'); - if (!collectionJsonContent) { - throw new Error('Could not find monorepo.json'); - } - const collectionJsonAst = parseJsonAst(collectionJsonContent.toString('utf-8')); - if (collectionJsonAst.kind !== 'object') { - throw new Error('Invalid monorepo content.'); - } - - const packages = collectionJsonAst.properties.find(x => x.key.value == 'packages'); - if (!packages) { - throw new Error('Cannot find packages key in monorepo.'); - } - if (packages.value.kind != 'object') { - throw new Error('Invalid packages key.'); - } - - const readmeUrl = `https://github.com/angular/angular-cli/blob/master/${path}/README.md`; - - const recorder = tree.beginUpdate('/.monorepo.json'); - appendPropertyInAstObject( - recorder, - packages.value, - options.name, - { - name: options.displayName, - links: [{ label: 'README', url: readmeUrl }], - version: '0.0.1', - hash: '', - }, - ); - tree.commitUpdate(recorder); - }; -} - - -export default function (options: Schema): Rule { - const path = 'packages/' - + options.name - .replace(/^@/, '') - .replace(/-/g, '_'); - - // Verify if we need to create a full project, or just add a new schematic. - const source = apply(url('https://github.com/project-files'), [ - template({ - ...options as object, - dot: '.', - path, - }), - ]); - - return chain([ - mergeWith(source), - addPackageToMonorepo(options, path), - ]); -} diff --git a/packages/_/devkit/package/project-files/__path__/README.md b/packages/_/devkit/package/project-files/__path__/README.md deleted file mode 100644 index f4b425475360..000000000000 --- a/packages/_/devkit/package/project-files/__path__/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# <%= displayName %> - -Work in progress diff --git a/packages/_/devkit/package/project-files/__path__/package.json b/packages/_/devkit/package/project-files/__path__/package.json deleted file mode 100644 index 7126e5f40667..000000000000 --- a/packages/_/devkit/package/project-files/__path__/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "<%= name %>", - "version": "0.0.0", - "description": "<%= description %>", - "main": "src/index.js", - "typings": "src/index.d.ts", - "scripts": { - "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" - }, - "keywords": [ - ], - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "0.0.0" - } -} diff --git a/packages/_/devkit/package/project-files/__path__/src/index.ts b/packages/_/devkit/package/project-files/__path__/src/index.ts deleted file mode 100644 index 258badcf91cd..000000000000 --- a/packages/_/devkit/package/project-files/__path__/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -// TODO: Make this useful (and awesome). -export default 1; diff --git a/packages/_/devkit/package/schema.json b/packages/_/devkit/package/schema.json deleted file mode 100644 index 4e8bca4c8627..000000000000 --- a/packages/_/devkit/package/schema.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "id": "SchematicsSchematicSchema", - "title": "DevKit Package Schematic Schema", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The package name for the new schematic.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "description": { - "type": "string", - "description": "The description of the new package" - }, - "displayName": { - "type": "string", - "$default": { - "$source": "interpolation", - "value": "${name}" - }, - "description": "The human readable name." - } - }, - "required": [ - "name", - "displayName" - ] -} diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel new file mode 100644 index 000000000000..de157d169bbb --- /dev/null +++ b/packages/angular/build/BUILD.bazel @@ -0,0 +1,362 @@ +load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") +load("//:constants.bzl", "BASELINE_DATE") +load("//tools:defaults.bzl", "copy_to_bin", "jasmine_test", "npm_package", "ts_project") +load("//tools:ts_json_schema.bzl", "ts_json_schema") +load("//tools/baseline_browserslist:baseline_browserslist.bzl", "baseline_browserslist") + +licenses(["notice"]) + +package(default_visibility = ["//visibility:public"]) + +npm_link_all_packages() + +ts_json_schema( + name = "application_schema", + src = "src/builders/application/schema.json", +) + +ts_json_schema( + name = "dev-server_schema", + src = "src/builders/dev-server/schema.json", +) + +ts_json_schema( + name = "extract_i18n_schema", + src = "src/builders/extract-i18n/schema.json", +) + +ts_json_schema( + name = "ng_karma_schema", + src = "src/builders/karma/schema.json", +) + +ts_json_schema( + name = "ng_packagr_schema", + src = "src/builders/ng-packagr/schema.json", +) + +ts_json_schema( + name = "unit_test_schema", + src = "src/builders/unit-test/schema.json", +) + +copy_to_bin( + name = "schemas", + srcs = glob(["**/schema.json"]), +) + +baseline_browserslist( + name = "angular_browserslist", + out = ".browserslistrc", + baseline = BASELINE_DATE, +) + +RUNTIME_ASSETS = glob( + include = [ + "src/**/schema.json", + "src/**/*.js", + "src/**/*.mjs", + "src/**/*.html", + ], +) + [ + "builders.json", + "package.json", + ":angular_browserslist", +] + +ts_project( + name = "build", + srcs = glob( + include = [ + "src/**/*.ts", + ], + exclude = [ + "src/test-utils.ts", + "src/**/*_spec.ts", + "src/**/tests/**/*.ts", + "src/testing/**/*.ts", + ], + ) + [ + "index.ts", + "//packages/angular/build:src/builders/application/schema.ts", + "//packages/angular/build:src/builders/dev-server/schema.ts", + "//packages/angular/build:src/builders/extract-i18n/schema.ts", + "//packages/angular/build:src/builders/karma/schema.ts", + "//packages/angular/build:src/builders/ng-packagr/schema.ts", + "//packages/angular/build:src/builders/unit-test/schema.ts", + ], + data = RUNTIME_ASSETS, + deps = [ + ":node_modules/@ampproject/remapping", + ":node_modules/@angular-devkit/architect", + ":node_modules/@angular-devkit/core", + ":node_modules/@angular/ssr", + ":node_modules/@babel/core", + ":node_modules/@babel/helper-annotate-as-pure", + ":node_modules/@babel/helper-split-export-declaration", + ":node_modules/@inquirer/confirm", + ":node_modules/@vitejs/plugin-basic-ssl", + ":node_modules/browserslist", + ":node_modules/https-proxy-agent", + ":node_modules/istanbul-lib-instrument", + ":node_modules/jsonc-parser", + ":node_modules/less", + ":node_modules/listr2", + ":node_modules/lmdb", + ":node_modules/magic-string", + ":node_modules/mrmime", + ":node_modules/ng-packagr", + ":node_modules/parse5-html-rewriting-stream", + ":node_modules/picomatch", + ":node_modules/piscina", + ":node_modules/postcss", + ":node_modules/sass", + ":node_modules/source-map-support", + ":node_modules/tinyglobby", + ":node_modules/vite", + ":node_modules/vitest", + ":node_modules/watchpack", + "//:node_modules/@angular/common", + "//:node_modules/@angular/compiler", + "//:node_modules/@angular/compiler-cli", + "//:node_modules/@angular/core", + "//:node_modules/@angular/localize", + "//:node_modules/@angular/platform-server", + "//:node_modules/@angular/service-worker", + "//:node_modules/@types/babel__core", + "//:node_modules/@types/karma", + "//:node_modules/@types/less", + "//:node_modules/@types/node", + "//:node_modules/@types/picomatch", + "//:node_modules/@types/semver", + "//:node_modules/@types/watchpack", + "//:node_modules/beasties", + "//:node_modules/esbuild", + "//:node_modules/esbuild-wasm", + "//:node_modules/karma", + "//:node_modules/rollup", + "//:node_modules/semver", + "//:node_modules/tslib", + "//:node_modules/typescript", + ], +) + +ts_project( + name = "unit_test_lib", + testonly = True, + srcs = glob( + include = ["src/**/*_spec.ts"], + exclude = ["src/builders/**/tests/**"], + ), + deps = [ + ":build", + ":node_modules/@angular-devkit/core", + ":node_modules/@babel/core", + "//:node_modules/@angular/compiler-cli", + "//:node_modules/@types/jasmine", + "//:node_modules/prettier", + "//:node_modules/typescript", + "//packages/angular/build/private", + ], +) + +jasmine_test( + name = "unit_tests", + data = [":unit_test_lib"], +) + +ts_project( + name = "application_integration_test_lib", + testonly = True, + srcs = glob(include = ["src/builders/application/tests/**/*.ts"]), + deps = [ + ":build", + "//packages/angular/build/private", + "//modules/testing/builder", + ":node_modules/@angular-devkit/architect", + ":node_modules/@angular-devkit/core", + + # Base dependencies for the application in hello-world-app. + "//:node_modules/@angular/common", + "//:node_modules/@angular/compiler", + "//:node_modules/@angular/compiler-cli", + "//:node_modules/@angular/core", + "//:node_modules/@angular/platform-browser", + "//:node_modules/@angular/router", + ":node_modules/rxjs", + "//:node_modules/tslib", + "//:node_modules/typescript", + "//:node_modules/zone.js", + "//:node_modules/buffer", + ], +) + +ts_project( + name = "dev-server_integration_test_lib", + testonly = True, + srcs = glob(include = ["src/builders/dev-server/tests/**/*.ts"]), + deps = [ + ":build", + "//packages/angular/build/private", + "//modules/testing/builder", + ":node_modules/@angular-devkit/architect", + ":node_modules/@angular-devkit/core", + + # dev server only test deps + "//:node_modules/@types/http-proxy", + "//:node_modules/@types/node", + "//:node_modules/http-proxy", + "//:node_modules/puppeteer", + + # Base dependencies for the application in hello-world-app. + "//:node_modules/@angular/common", + "//:node_modules/@angular/compiler", + "//:node_modules/@angular/compiler-cli", + "//:node_modules/@angular/core", + "//:node_modules/@angular/platform-browser", + "//:node_modules/@angular/router", + ":node_modules/ng-packagr", + ":node_modules/rxjs", + "//:node_modules/tslib", + "//:node_modules/typescript", + "//:node_modules/zone.js", + "//:node_modules/buffer", + ], +) + +ts_project( + name = "karma_integration_test_lib", + testonly = True, + srcs = glob(include = ["src/builders/karma/tests/**/*.ts"]), + deps = [ + ":build", + "//packages/angular/build/private", + "//modules/testing/builder", + ":node_modules/@angular-devkit/architect", + ":node_modules/@angular-devkit/core", + "//:node_modules/@types/node", + + # karma specific test deps + "//:node_modules/karma-chrome-launcher", + "//:node_modules/karma-coverage", + "//:node_modules/karma-jasmine", + "//:node_modules/karma-jasmine-html-reporter", + "//:node_modules/puppeteer", + + # Base dependencies for the karma in hello-world-app. + "//:node_modules/@angular/common", + "//:node_modules/@angular/compiler", + "//:node_modules/@angular/compiler-cli", + "//:node_modules/@angular/core", + "//:node_modules/@angular/platform-browser", + "//:node_modules/@angular/router", + ":node_modules/rxjs", + "//:node_modules/tslib", + "//:node_modules/typescript", + "//:node_modules/zone.js", + "//:node_modules/buffer", + ], +) + +ts_project( + name = "unit-test_integration_test_lib", + testonly = True, + srcs = glob(include = ["src/builders/unit-test/tests/**/*.ts"]), + deps = [ + ":build", + "//packages/angular/build/private", + "//modules/testing/builder", + ":node_modules/@angular-devkit/architect", + ":node_modules/@angular-devkit/core", + "//:node_modules/@types/node", + + # unit test specific test deps + ":node_modules/vitest", + ":node_modules/jsdom", + + # Base dependencies for the hello-world-app. + "//:node_modules/@angular/common", + "//:node_modules/@angular/compiler", + "//:node_modules/@angular/compiler-cli", + "//:node_modules/@angular/core", + "//:node_modules/@angular/platform-browser", + "//:node_modules/@angular/router", + ":node_modules/rxjs", + "//:node_modules/tslib", + "//:node_modules/typescript", + "//:node_modules/zone.js", + ], +) + +jasmine_test( + name = "application_integration_tests", + size = "large", + data = [":application_integration_test_lib"], + flaky = True, + shard_count = 10, +) + +jasmine_test( + name = "dev-server_integration_tests", + size = "large", + data = [":dev-server_integration_test_lib"], + flaky = True, + shard_count = 10, +) + +jasmine_test( + name = "karma_integration_tests", + size = "large", + data = [":karma_integration_test_lib"], + env = { + # TODO: Replace Puppeteer downloaded browsers with Bazel-managed browsers, + # or standardize to avoid complex configuration like this! + "PUPPETEER_DOWNLOAD_PATH": "../../../node_modules/puppeteer/downloads", + }, + flaky = True, + shard_count = 10, +) + +jasmine_test( + name = "unit-test_integration_tests", + size = "large", + data = [":unit-test_integration_test_lib"], + shard_count = 10, +) + +genrule( + name = "license", + srcs = ["//:LICENSE"], + outs = ["LICENSE"], + cmd = "cp $(execpath //:LICENSE) $@", +) + +npm_package( + name = "pkg", + pkg_deps = [ + "//packages/angular_devkit/architect:package.json", + ], + stamp_files = [ + "src/utils/version.js", + "src/tools/esbuild/utils.js", + "src/utils/normalize-cache.js", + ], + tags = ["release-package"], + deps = RUNTIME_ASSETS + [ + ":README.md", + ":build", + ":license", + "//packages/angular/build/private", + ], +) + +api_golden_test_npm_package( + name = "api", + data = [ + ":npm_package", + "//goldens:public-api", + ], + golden_dir = "goldens/public-api/angular/build", + npm_package = "packages/angular/build/npm_package", +) diff --git a/packages/angular/build/README.md b/packages/angular/build/README.md new file mode 100644 index 000000000000..62249f7fe422 --- /dev/null +++ b/packages/angular/build/README.md @@ -0,0 +1,5 @@ +# Angular Build System for Applications and Libraries + +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/build/builders.json b/packages/angular/build/builders.json new file mode 100644 index 000000000000..4dc9c9c245a6 --- /dev/null +++ b/packages/angular/build/builders.json @@ -0,0 +1,34 @@ +{ + "builders": { + "application": { + "implementation": "./src/builders/application/index", + "schema": "./src/builders/application/schema.json", + "description": "Build an application." + }, + "dev-server": { + "implementation": "./src/builders/dev-server/index", + "schema": "./src/builders/dev-server/schema.json", + "description": "Execute a development server for an application." + }, + "extract-i18n": { + "implementation": "./src/builders/extract-i18n/index", + "schema": "./src/builders/extract-i18n/schema.json", + "description": "Extract i18n messages from an application." + }, + "karma": { + "implementation": "./src/builders/karma", + "schema": "./src/builders/karma/schema.json", + "description": "Run Karma unit tests." + }, + "ng-packagr": { + "implementation": "./src/builders/ng-packagr/index", + "schema": "./src/builders/ng-packagr/schema.json", + "description": "Build a library with ng-packagr." + }, + "unit-test": { + "implementation": "./src/builders/unit-test", + "schema": "./src/builders/unit-test/schema.json", + "description": "[EXPERIMENTAL] Run application unit tests." + } + } +} diff --git a/packages/angular/build/index.ts b/packages/angular/build/index.ts new file mode 100644 index 000000000000..e6da94cc7ded --- /dev/null +++ b/packages/angular/build/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export * from './src/index'; diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json new file mode 100644 index 000000000000..55c90305eb2a --- /dev/null +++ b/packages/angular/build/package.json @@ -0,0 +1,117 @@ +{ + "name": "@angular/build", + "version": "0.0.0-PLACEHOLDER", + "description": "Official build system for Angular", + "keywords": [ + "angular", + "angular-cli" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "default": "./src/index.js" + }, + "./private": { + "default": "./src/private.js" + }, + "./package.json": "./package.json" + }, + "builders": "builders.json", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "workspace:0.0.0-EXPERIMENTAL-PLACEHOLDER", + "@babel/core": "7.27.1", + "@babel/helper-annotate-as-pure": "7.27.1", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.9", + "@vitejs/plugin-basic-ssl": "2.0.0", + "beasties": "0.3.3", + "browserslist": "^4.23.0", + "esbuild": "0.25.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "8.3.2", + "magic-string": "0.30.17", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "7.1.0", + "picomatch": "4.0.2", + "piscina": "4.9.2", + "rollup": "4.40.1", + "sass": "1.87.0", + "semver": "7.7.1", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.13", + "vite": "6.3.4", + "watchpack": "2.4.2" + }, + "optionalDependencies": { + "lmdb": "3.2.6" + }, + "devDependencies": { + "@angular/ssr": "workspace:*", + "@angular-devkit/core": "workspace:*", + "jsdom": "26.1.0", + "less": "4.3.0", + "ng-packagr": "20.0.0-rc.0", + "postcss": "8.5.3", + "rxjs": "7.8.2", + "vitest": "3.1.2" + }, + "peerDependencies": { + "@angular/core": "0.0.0-ANGULAR-FW-PEER-DEP", + "@angular/compiler": "0.0.0-ANGULAR-FW-PEER-DEP", + "@angular/compiler-cli": "0.0.0-ANGULAR-FW-PEER-DEP", + "@angular/localize": "0.0.0-ANGULAR-FW-PEER-DEP", + "@angular/platform-browser": "0.0.0-ANGULAR-FW-PEER-DEP", + "@angular/platform-server": "0.0.0-ANGULAR-FW-PEER-DEP", + "@angular/service-worker": "0.0.0-ANGULAR-FW-PEER-DEP", + "@angular/ssr": "^0.0.0-PLACEHOLDER", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "0.0.0-NG-PACKAGR-PEER-DEP", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.8 <5.9", + "vitest": "^3.1.1" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } +} diff --git a/packages/angular/build/private/BUILD.bazel b/packages/angular/build/private/BUILD.bazel new file mode 100644 index 000000000000..c3a100de897f --- /dev/null +++ b/packages/angular/build/private/BUILD.bazel @@ -0,0 +1,11 @@ +load("//tools:defaults.bzl", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "private", + srcs = ["index.ts"], + deps = [ + "//packages/angular/build", + ], +) diff --git a/packages/angular/build/private/index.ts b/packages/angular/build/private/index.ts new file mode 100644 index 000000000000..1c2b76656baf --- /dev/null +++ b/packages/angular/build/private/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export * from '../src/private'; diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts new file mode 100644 index 000000000000..c59863f0ebf5 --- /dev/null +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -0,0 +1,435 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 { existsSync } from 'node:fs'; +import path from 'node:path'; +import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context'; +import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result'; +import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language'; +import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils'; +import { ChangedFiles } from '../../tools/esbuild/watcher'; +import { shouldWatchRoot } from '../../utils/environment-options'; +import { NormalizedCachedOptions } from '../../utils/normalize-cache'; +import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options'; +import { + ComponentUpdateResult, + FullResult, + IncrementalResult, + Result, + ResultKind, + ResultMessage, +} from './results'; + +// Watch workspace for package manager changes +const packageWatchFiles = [ + // manifest can affect module resolution + 'package.json', + // npm lock file + 'package-lock.json', + // pnpm lock file + 'pnpm-lock.yaml', + // yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/) + 'yarn.lock', + '.pnp.cjs', + '.pnp.data.json', +]; + +export async function* runEsBuildBuildAction( + action: (rebuildState?: RebuildState) => Promise, + options: { + workspaceRoot: string; + projectRoot: string; + outputOptions: NormalizedOutputOptions; + logger: BuilderContext['logger']; + cacheOptions: NormalizedCachedOptions; + watch?: boolean; + verbose?: boolean; + progress?: boolean; + poll?: number; + signal?: AbortSignal; + preserveSymlinks?: boolean; + clearScreen?: boolean; + colors?: boolean; + jsonLogs?: boolean; + incrementalResults?: boolean; + }, +): AsyncIterable { + const { + watch, + poll, + clearScreen, + logger, + cacheOptions, + outputOptions, + verbose, + projectRoot, + workspaceRoot, + progress, + preserveSymlinks, + colors, + jsonLogs, + incrementalResults, + } = options; + + const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress; + + // Initial build + let result: ExecutionResult; + try { + // Perform the build action + result = await withProgress('Building...', () => action()); + + // Log all diagnostic (error/warning/logs) messages + await logMessages(logger, result, colors, jsonLogs); + } finally { + // Ensure Sass workers are shutdown if not watching + if (!watch) { + shutdownSassWorkerPool(); + } + } + + // Setup watcher if watch mode enabled + let watcher: import('../../tools/esbuild/watcher').BuildWatcher | undefined; + if (watch) { + if (progress) { + logger.info('Watch mode enabled. Watching for file changes...'); + } + + const ignored: string[] = [ + // Ignore the output and cache paths to avoid infinite rebuild cycles + outputOptions.base, + cacheOptions.basePath, + `${workspaceRoot.replace(/\\/g, '/')}/**/.*/**`, + ]; + + // Setup a watcher + const { createWatcher } = await import('../../tools/esbuild/watcher'); + watcher = createWatcher({ + polling: typeof poll === 'number', + interval: poll, + followSymlinks: preserveSymlinks, + ignored, + }); + + // Setup abort support + options.signal?.addEventListener('abort', () => void watcher?.close()); + + // Watch the entire project root if 'NG_BUILD_WATCH_ROOT' environment variable is set + if (shouldWatchRoot) { + if (!preserveSymlinks) { + // Ignore all node modules directories to avoid excessive file watchers. + // Package changes are handled below by watching manifest and lock files. + // NOTE: this is not enable when preserveSymlinks is true as this would break `npm link` usages. + ignored.push('**/node_modules/**'); + + watcher.add( + packageWatchFiles + .map((file) => path.join(workspaceRoot, file)) + .filter((file) => existsSync(file)), + ); + } + + watcher.add(projectRoot); + } + + // Watch locations provided by the initial build result + watcher.add(result.watchFiles); + } + + // Output the first build results after setting up the watcher to ensure that any code executed + // higher in the iterator call stack will trigger the watcher. This is particularly relevant for + // unit tests which execute the builder and modify the file system programmatically. + yield* emitOutputResults(result, outputOptions); + + // Finish if watch mode is not enabled + if (!watcher) { + return; + } + + // Used to force a full result on next rebuild if there were initial errors. + // This ensures at least one full result is emitted. + let hasInitialErrors = result.errors.length > 0; + + // Wait for changes and rebuild as needed + const currentWatchFiles = new Set(result.watchFiles); + try { + for await (const changes of watcher) { + if (options.signal?.aborted) { + break; + } + + if (clearScreen) { + // eslint-disable-next-line no-console + console.clear(); + } + + if (verbose) { + logger.info(changes.toDebugString()); + } + + // Clear removed files from current watch files + changes.removed.forEach((removedPath) => currentWatchFiles.delete(removedPath)); + + const rebuildState = result.createRebuildState(changes); + result = await withProgress('Changes detected. Rebuilding...', () => action(rebuildState)); + + // Log all diagnostic (error/warning/logs) messages + await logMessages(logger, result, colors, jsonLogs); + + // Update watched locations provided by the new build result. + // Keep watching all previous files if there are any errors; otherwise consider all + // files stale until confirmed present in the new result's watch files. + const staleWatchFiles = result.errors.length > 0 ? undefined : new Set(currentWatchFiles); + for (const watchFile of result.watchFiles) { + if (!currentWatchFiles.has(watchFile)) { + // Add new watch location + watcher.add(watchFile); + currentWatchFiles.add(watchFile); + } + + // Present so remove from stale locations + staleWatchFiles?.delete(watchFile); + } + // Remove any stale locations if the build was successful + if (staleWatchFiles?.size) { + watcher.remove([...staleWatchFiles]); + } + + for (const outputResult of emitOutputResults( + result, + outputOptions, + changes, + incrementalResults && !hasInitialErrors ? rebuildState : undefined, + )) { + yield outputResult; + } + + // Clear initial build errors flag if no errors are now present + hasInitialErrors &&= result.errors.length > 0; + } + } finally { + // Stop the watcher and cleanup incremental rebuild state + await Promise.allSettled([watcher.close(), result.dispose()]); + + shutdownSassWorkerPool(); + } +} + +function* emitOutputResults( + { + outputFiles, + assetFiles, + errors, + warnings, + externalMetadata, + htmlIndexPath, + htmlBaseHref, + templateUpdates, + }: ExecutionResult, + outputOptions: NormalizedApplicationBuildOptions['outputOptions'], + changes?: ChangedFiles, + rebuildState?: RebuildState, +): Iterable { + if (errors.length > 0) { + yield { + kind: ResultKind.Failure, + errors: errors as ResultMessage[], + warnings: warnings as ResultMessage[], + detail: { + outputOptions, + }, + }; + + return; + } + + // Use a full result if there is no rebuild state (no prior build result) + if (!rebuildState || !changes) { + const result: FullResult = { + kind: ResultKind.Full, + warnings: warnings as ResultMessage[], + files: {}, + detail: { + externalMetadata, + htmlIndexPath, + htmlBaseHref, + outputOptions, + }, + }; + for (const file of assetFiles) { + result.files[file.destination] = { + type: BuildOutputFileType.Browser, + inputPath: file.source, + origin: 'disk', + }; + } + for (const file of outputFiles) { + result.files[file.path] = { + type: file.type, + contents: file.contents, + origin: 'memory', + hash: file.hash, + }; + } + + yield result; + + return; + } + + // Template updates only exist if no other JS changes have occurred. + // A full page reload may be required based on the following incremental output change analysis. + const hasTemplateUpdates = !!templateUpdates?.size; + + // Use an incremental result if previous output information is available + const { previousAssetsInfo, previousOutputInfo } = rebuildState; + + const incrementalResult: IncrementalResult = { + kind: ResultKind.Incremental, + warnings: warnings as ResultMessage[], + // Initially attempt to use a background update of files to support component updates. + background: hasTemplateUpdates, + added: [], + removed: [], + modified: [], + files: {}, + detail: { + externalMetadata, + htmlIndexPath, + htmlBaseHref, + outputOptions, + }, + }; + + let hasCssUpdates = false; + + // Initially assume all previous output files have been removed + const removedOutputFiles = new Map(previousOutputInfo); + for (const file of outputFiles) { + removedOutputFiles.delete(file.path); + + const previousHash = previousOutputInfo.get(file.path)?.hash; + let needFile = false; + if (previousHash === undefined) { + needFile = true; + incrementalResult.added.push(file.path); + } else if (previousHash !== file.hash) { + needFile = true; + incrementalResult.modified.push(file.path); + } + + if (needFile) { + if (file.path.endsWith('.css')) { + hasCssUpdates = true; + } else if (!canBackgroundUpdate(file)) { + incrementalResult.background = false; + } + + incrementalResult.files[file.path] = { + type: file.type, + contents: file.contents, + origin: 'memory', + hash: file.hash, + }; + } + } + + // Initially assume all previous assets files have been removed + const removedAssetFiles = new Map(previousAssetsInfo); + for (const { source, destination } of assetFiles) { + removedAssetFiles.delete(source); + + if (!previousAssetsInfo.has(source)) { + incrementalResult.added.push(destination); + incrementalResult.background = false; + } else if (changes.modified.has(source)) { + incrementalResult.modified.push(destination); + incrementalResult.background = false; + } else { + continue; + } + + hasCssUpdates ||= destination.endsWith('.css'); + + incrementalResult.files[destination] = { + type: BuildOutputFileType.Browser, + inputPath: source, + origin: 'disk', + }; + } + + // Do not remove stale files yet if there are template updates. + // Component chunk files may still be referenced in running browser code. + // Module evaluation time component updates will update any of these files. + // This typically occurs when a lazy component is changed that has not yet + // been accessed at runtime. + if (hasTemplateUpdates && incrementalResult.background) { + removedOutputFiles.clear(); + } + + // Include the removed output and asset files + incrementalResult.removed.push( + ...Array.from(removedOutputFiles, ([file, { type }]) => ({ + path: file, + type, + })), + ...Array.from(removedAssetFiles.values(), (file) => ({ + path: file, + type: BuildOutputFileType.Browser, + })), + ); + + yield incrementalResult; + + // If there are template updates and the incremental update was background only, a component + // update is possible. + if (hasTemplateUpdates && incrementalResult.background) { + // Template changes may be accompanied by stylesheet changes and these should also be updated hot when possible. + if (hasCssUpdates) { + const styleResult: IncrementalResult = { + kind: ResultKind.Incremental, + added: incrementalResult.added.filter(isCssFilePath), + removed: incrementalResult.removed.filter(({ path }) => isCssFilePath(path)), + modified: incrementalResult.modified.filter(isCssFilePath), + files: Object.fromEntries( + Object.entries(incrementalResult.files).filter(([path]) => isCssFilePath(path)), + ), + }; + + yield styleResult; + } + + const updateResult: ComponentUpdateResult = { + kind: ResultKind.ComponentUpdate, + updates: Array.from(templateUpdates, ([id, content]) => ({ + type: 'template', + id, + content, + })), + }; + + yield updateResult; + } +} + +function isCssFilePath(filePath: string): boolean { + return /\.css(?:\.map)?$/i.test(filePath); +} + +function canBackgroundUpdate(file: BuildOutputFile): boolean { + // Files in the output root are not served and do not affect the + // application available with the development server. + if (file.type === BuildOutputFileType.Root) { + return true; + } + + // Updates to non-JS files must signal an update with the dev server + // except the service worker configuration which is special cased. + return /(?:\.m?js|\.map)$/.test(file.path) || file.path === 'ngsw.json'; +} diff --git a/packages/angular/build/src/builders/application/chunk-optimizer.ts b/packages/angular/build/src/builders/application/chunk-optimizer.ts new file mode 100644 index 000000000000..a0ffa657b5c3 --- /dev/null +++ b/packages/angular/build/src/builders/application/chunk-optimizer.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 assert from 'node:assert'; +import { rollup } from 'rollup'; +import { + BuildOutputFile, + BuildOutputFileType, + BundleContextResult, + InitialFileRecord, +} from '../../tools/esbuild/bundler-context'; +import { createOutputFile } from '../../tools/esbuild/utils'; +import { assertIsError } from '../../utils/error'; + +export async function optimizeChunks( + original: BundleContextResult, + sourcemap: boolean | 'hidden', +): Promise { + // Failed builds cannot be optimized + if (original.errors) { + return original; + } + + // Find the main browser entrypoint + let mainFile; + for (const [file, record] of original.initialFiles) { + if ( + record.name === 'main' && + record.entrypoint && + !record.serverFile && + record.type === 'script' + ) { + mainFile = file; + break; + } + } + + // No action required if no browser main entrypoint + if (!mainFile) { + return original; + } + + const chunks: Record = {}; + const maps: Record = {}; + for (const originalFile of original.outputFiles) { + if (originalFile.type !== BuildOutputFileType.Browser) { + continue; + } + + if (originalFile.path.endsWith('.js')) { + chunks[originalFile.path] = originalFile; + } else if (originalFile.path.endsWith('.js.map')) { + // Create mapping of JS file to sourcemap content + maps[originalFile.path.slice(0, -4)] = originalFile; + } + } + + const usedChunks = new Set(); + + let bundle; + let optimizedOutput; + try { + bundle = await rollup({ + input: mainFile, + plugins: [ + { + name: 'angular-bundle', + resolveId(source) { + // Remove leading `./` if present + const file = source[0] === '.' && source[1] === '/' ? source.slice(2) : source; + + if (chunks[file]) { + return file; + } + + // All other identifiers are considered external to maintain behavior + return { id: source, external: true }; + }, + load(id) { + assert( + chunks[id], + `Angular chunk content should always be present in chunk optimizer [${id}].`, + ); + + usedChunks.add(id); + + const result = { + code: chunks[id].text, + map: maps[id]?.text, + }; + + return result; + }, + }, + ], + }); + + const result = await bundle.generate({ + compact: true, + sourcemap, + chunkFileNames: (chunkInfo) => `${chunkInfo.name.replace(/-[a-zA-Z0-9]{8}$/, '')}-[hash].js`, + }); + optimizedOutput = result.output; + } catch (e) { + assertIsError(e); + + return { + errors: [ + // Most of these fields are not actually needed for printing the error + { + id: '', + text: 'Chunk optimization failed', + detail: undefined, + pluginName: '', + location: null, + notes: [ + { + text: e.message, + location: null, + }, + ], + }, + ], + warnings: original.warnings, + }; + } finally { + await bundle?.close(); + } + + // Remove used chunks and associated sourcemaps from the original result + original.outputFiles = original.outputFiles.filter( + (file) => + !usedChunks.has(file.path) && + !(file.path.endsWith('.map') && usedChunks.has(file.path.slice(0, -4))), + ); + + // Add new optimized chunks + const importsPerFile: Record = {}; + for (const optimizedFile of optimizedOutput) { + if (optimizedFile.type !== 'chunk') { + continue; + } + + importsPerFile[optimizedFile.fileName] = optimizedFile.imports; + + original.outputFiles.push( + createOutputFile(optimizedFile.fileName, optimizedFile.code, BuildOutputFileType.Browser), + ); + if (optimizedFile.map && optimizedFile.sourcemapFileName) { + original.outputFiles.push( + createOutputFile( + optimizedFile.sourcemapFileName, + optimizedFile.map.toString(), + BuildOutputFileType.Browser, + ), + ); + } + } + + // Update initial files to reflect optimized chunks + const entriesToAnalyze: [string, InitialFileRecord][] = []; + for (const usedFile of usedChunks) { + // Leave the main file since its information did not change + if (usedFile === mainFile) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + entriesToAnalyze.push([mainFile, original.initialFiles.get(mainFile)!]); + continue; + } + + // Remove all other used chunks + original.initialFiles.delete(usedFile); + } + + // Analyze for transitive initial files + let currentEntry; + while ((currentEntry = entriesToAnalyze.pop())) { + const [entryPath, entryRecord] = currentEntry; + + for (const importPath of importsPerFile[entryPath]) { + const existingRecord = original.initialFiles.get(importPath); + if (existingRecord) { + // Store the smallest value depth + if (existingRecord.depth > entryRecord.depth + 1) { + existingRecord.depth = entryRecord.depth + 1; + } + + continue; + } + + const record: InitialFileRecord = { + type: 'script', + entrypoint: false, + external: false, + serverFile: false, + depth: entryRecord.depth + 1, + }; + + entriesToAnalyze.push([importPath, record]); + } + } + + return original; +} diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts new file mode 100644 index 000000000000..72a07d8b8307 --- /dev/null +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -0,0 +1,329 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 { createAngularCompilation } from '../../tools/angular/compilation'; +import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache'; +import { generateBudgetStats } from '../../tools/esbuild/budget-stats'; +import { + BuildOutputFileType, + BundleContextResult, + BundlerContext, +} from '../../tools/esbuild/bundler-context'; +import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result'; +import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker'; +import { extractLicenses } from '../../tools/esbuild/license-extractor'; +import { profileAsync } from '../../tools/esbuild/profiling'; +import { + calculateEstimatedTransferSizes, + logBuildStats, + transformSupportedBrowsersToTargets, +} from '../../tools/esbuild/utils'; +import { BudgetCalculatorResult, checkBudgets } from '../../utils/bundle-calculator'; +import { shouldOptimizeChunks } from '../../utils/environment-options'; +import { resolveAssets } from '../../utils/resolve-assets'; +import { + SERVER_APP_ENGINE_MANIFEST_FILENAME, + generateAngularServerAppEngineManifest, +} from '../../utils/server-rendering/manifest'; +import { getSupportedBrowsers } from '../../utils/supported-browsers'; +import { executePostBundleSteps } from './execute-post-bundle'; +import { inlineI18n, loadActiveTranslations } from './i18n'; +import { NormalizedApplicationBuildOptions } from './options'; +import { createComponentStyleBundler, setupBundlerContexts } from './setup-bundling'; + +// eslint-disable-next-line max-lines-per-function +export async function executeBuild( + options: NormalizedApplicationBuildOptions, + context: BuilderContext, + rebuildState?: RebuildState, +): Promise { + const { + projectRoot, + workspaceRoot, + i18nOptions, + optimizationOptions, + assets, + cacheOptions, + serverEntryPoint, + baseHref, + ssrOptions, + verbose, + colors, + jsonLogs, + } = options; + + // TODO: Consider integrating into watch mode. Would require full rebuild on target changes. + const browsers = getSupportedBrowsers(projectRoot, context.logger); + + // Load active translations if inlining + // TODO: Integrate into watch mode and only load changed translations + if (i18nOptions.shouldInline) { + await loadActiveTranslations(context, i18nOptions); + } + + // Reuse rebuild state or create new bundle contexts for code and global stylesheets + let bundlerContexts; + let componentStyleBundler; + let codeBundleCache; + let bundlingResult: BundleContextResult; + let templateUpdates: Map | undefined; + if (rebuildState) { + bundlerContexts = rebuildState.rebuildContexts; + componentStyleBundler = rebuildState.componentStyleBundler; + codeBundleCache = rebuildState.codeBundleCache; + templateUpdates = rebuildState.templateUpdates; + // Reset template updates for new rebuild + templateUpdates?.clear(); + + const allFileChanges = rebuildState.fileChanges.all; + + // Bundle all contexts that do not require TypeScript changed file checks. + // These will automatically use cached results based on the changed files. + bundlingResult = await BundlerContext.bundleAll(bundlerContexts.otherContexts, allFileChanges); + + // Check the TypeScript code bundling cache for changes. If invalid, force a rebundle of + // all TypeScript related contexts. + const forceTypeScriptRebuild = codeBundleCache?.invalidate(allFileChanges); + const typescriptResults: BundleContextResult[] = []; + for (const typescriptContext of bundlerContexts.typescriptContexts) { + typescriptContext.invalidate(allFileChanges); + const result = await typescriptContext.bundle(forceTypeScriptRebuild); + typescriptResults.push(result); + } + bundlingResult = BundlerContext.mergeResults([bundlingResult, ...typescriptResults]); + } else { + const target = transformSupportedBrowsersToTargets(browsers); + codeBundleCache = new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined); + componentStyleBundler = createComponentStyleBundler(options, target); + if (options.templateUpdates) { + templateUpdates = new Map(); + } + bundlerContexts = setupBundlerContexts( + options, + target, + codeBundleCache, + componentStyleBundler, + // Create new reusable compilation for the appropriate mode based on the `jit` plugin option + await createAngularCompilation(!!options.jit, !options.serverEntryPoint), + templateUpdates, + ); + + // Bundle everything on initial build + bundlingResult = await BundlerContext.bundleAll([ + ...bundlerContexts.typescriptContexts, + ...bundlerContexts.otherContexts, + ]); + } + + // Update any external component styles if enabled and rebuilding. + // TODO: Only attempt rebundling of invalidated styles once incremental build results are supported. + if (rebuildState && options.externalRuntimeStyles) { + componentStyleBundler.invalidate(rebuildState.fileChanges.all); + + const componentResults = await componentStyleBundler.bundleAllFiles(true, true); + bundlingResult = BundlerContext.mergeResults([bundlingResult, ...componentResults]); + } + + if (options.optimizationOptions.scripts && shouldOptimizeChunks) { + const { optimizeChunks } = await import('./chunk-optimizer'); + bundlingResult = await profileAsync('OPTIMIZE_CHUNKS', () => + optimizeChunks( + bundlingResult, + options.sourcemapOptions.scripts ? !options.sourcemapOptions.hidden || 'hidden' : false, + ), + ); + } + + const executionResult = new ExecutionResult( + bundlerContexts, + componentStyleBundler, + codeBundleCache, + templateUpdates, + ); + executionResult.addWarnings(bundlingResult.warnings); + + // Add used external component style referenced files to be watched + if (options.externalRuntimeStyles) { + executionResult.extraWatchFiles.push(...componentStyleBundler.collectReferencedFiles()); + } + + // Return if the bundling has errors + if (bundlingResult.errors) { + executionResult.addErrors(bundlingResult.errors); + + return executionResult; + } + + // Analyze external imports if external options are enabled + if (options.externalPackages || bundlingResult.externalConfiguration) { + const { + externalConfiguration = [], + externalImports: { browser = [], server = [] }, + } = bundlingResult; + // Similar to esbuild, --external:@foo/bar automatically implies --external:@foo/bar/*, + // which matches import paths like @foo/bar/baz. + // This means all paths within the @foo/bar package are also marked as external. + const exclusionsPrefixes = externalConfiguration.map((exclusion) => exclusion + '/'); + const exclusions = new Set(externalConfiguration); + const explicitExternal = new Set(); + + const isExplicitExternal = (dep: string): boolean => { + if (exclusions.has(dep)) { + return true; + } + + for (const prefix of exclusionsPrefixes) { + if (dep.startsWith(prefix)) { + return true; + } + } + + return false; + }; + + const implicitBrowser: string[] = []; + for (const dep of browser) { + if (isExplicitExternal(dep)) { + explicitExternal.add(dep); + } else { + implicitBrowser.push(dep); + } + } + + const implicitServer: string[] = []; + for (const dep of server) { + if (isExplicitExternal(dep)) { + explicitExternal.add(dep); + } else { + implicitServer.push(dep); + } + } + + executionResult.setExternalMetadata(implicitBrowser, implicitServer, [...explicitExternal]); + } + + const { metafile, initialFiles, outputFiles } = bundlingResult; + + executionResult.outputFiles.push(...outputFiles); + + // Analyze files for bundle budget failures if present + let budgetFailures: BudgetCalculatorResult[] | undefined; + if (options.budgets) { + const compatStats = generateBudgetStats(metafile, outputFiles, initialFiles); + budgetFailures = [...checkBudgets(options.budgets, compatStats, true)]; + for (const { message, severity } of budgetFailures) { + if (severity === 'error') { + executionResult.addError(message); + } else { + executionResult.addWarning(message); + } + } + } + + // Calculate estimated transfer size if scripts are optimized + let estimatedTransferSizes; + if (optimizationOptions.scripts || optimizationOptions.styles.minify) { + estimatedTransferSizes = await calculateEstimatedTransferSizes(executionResult.outputFiles); + } + + // Check metafile for CommonJS module usage if optimizing scripts + if (optimizationOptions.scripts) { + const messages = checkCommonJSModules(metafile, options.allowedCommonJsDependencies); + executionResult.addWarnings(messages); + } + + // Copy assets + if (assets) { + executionResult.addAssets(await resolveAssets(assets, workspaceRoot)); + } + + // Extract and write licenses for used packages + if (options.extractLicenses) { + executionResult.addOutputFile( + '3rdpartylicenses.txt', + await extractLicenses(metafile, workspaceRoot), + BuildOutputFileType.Root, + ); + } + + // Watch input index HTML file if configured + if (options.indexHtmlOptions) { + executionResult.extraWatchFiles.push(options.indexHtmlOptions.input); + executionResult.htmlIndexPath = options.indexHtmlOptions.output; + executionResult.htmlBaseHref = options.baseHref; + } + + // Create server app engine manifest + if (serverEntryPoint) { + executionResult.addOutputFile( + SERVER_APP_ENGINE_MANIFEST_FILENAME, + generateAngularServerAppEngineManifest(i18nOptions, baseHref), + BuildOutputFileType.ServerRoot, + ); + } + + // Perform i18n translation inlining if enabled + if (i18nOptions.shouldInline) { + const result = await inlineI18n(metafile, options, executionResult, initialFiles); + executionResult.addErrors(result.errors); + executionResult.addWarnings(result.warnings); + executionResult.addPrerenderedRoutes(result.prerenderedRoutes); + } else { + const result = await executePostBundleSteps( + metafile, + options, + executionResult.outputFiles, + executionResult.assetFiles, + initialFiles, + // Set lang attribute to the defined source locale if present + i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined, + ); + + executionResult.addErrors(result.errors); + executionResult.addWarnings(result.warnings); + executionResult.addPrerenderedRoutes(result.prerenderedRoutes); + executionResult.outputFiles.push(...result.additionalOutputFiles); + executionResult.assetFiles.push(...result.additionalAssets); + } + + executionResult.addOutputFile( + 'prerendered-routes.json', + JSON.stringify({ routes: executionResult.prerenderedRoutes }, null, 2), + BuildOutputFileType.Root, + ); + + // Write metafile if stats option is enabled + if (options.stats) { + executionResult.addOutputFile( + 'stats.json', + JSON.stringify(metafile, null, 2), + BuildOutputFileType.Root, + ); + } + + if (!jsonLogs) { + const changedFiles = + rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo); + executionResult.addLog( + logBuildStats( + metafile, + outputFiles, + initialFiles, + budgetFailures, + colors, + changedFiles, + estimatedTransferSizes, + !!ssrOptions, + verbose, + ), + ); + } + + return executionResult; +} diff --git a/packages/angular/build/src/builders/application/execute-post-bundle.ts b/packages/angular/build/src/builders/application/execute-post-bundle.ts new file mode 100644 index 000000000000..5171ca254d5d --- /dev/null +++ b/packages/angular/build/src/builders/application/execute-post-bundle.ts @@ -0,0 +1,257 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 assert from 'node:assert'; +import { + BuildOutputFile, + BuildOutputFileType, + InitialFileRecord, +} from '../../tools/esbuild/bundler-context'; +import { + BuildOutputAsset, + PrerenderedRoutesRecord, +} from '../../tools/esbuild/bundler-execution-result'; +import { generateIndexHtml } from '../../tools/esbuild/index-html-generator'; +import { createOutputFile } from '../../tools/esbuild/utils'; +import { maxWorkers } from '../../utils/environment-options'; +import { + SERVER_APP_MANIFEST_FILENAME, + generateAngularServerAppManifest, +} from '../../utils/server-rendering/manifest'; +import { + RouteRenderMode, + WritableSerializableRouteTreeNode, +} from '../../utils/server-rendering/models'; +import { prerenderPages } from '../../utils/server-rendering/prerender'; +import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker'; +import { INDEX_HTML_CSR, INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options'; +import { OutputMode } from './schema'; + +/** + * Run additional builds steps including SSG, AppShell, Index HTML file and Service worker generation. + * @param metafile An esbuild metafile object. + * @param options The normalized application builder options used to create the build. + * @param outputFiles The output files of an executed build. + * @param assetFiles The assets of an executed build. + * @param initialFiles A map containing initial file information for the executed build. + * @param locale A language locale to insert in the index.html. + */ +// eslint-disable-next-line max-lines-per-function +export async function executePostBundleSteps( + metafile: Metafile, + options: NormalizedApplicationBuildOptions, + outputFiles: BuildOutputFile[], + assetFiles: BuildOutputAsset[], + initialFiles: Map, + locale: string | undefined, +): Promise<{ + errors: string[]; + warnings: string[]; + additionalOutputFiles: BuildOutputFile[]; + additionalAssets: BuildOutputAsset[]; + prerenderedRoutes: PrerenderedRoutesRecord; +}> { + const additionalAssets: BuildOutputAsset[] = []; + const additionalOutputFiles: BuildOutputFile[] = []; + const allErrors: string[] = []; + const allWarnings: string[] = []; + const prerenderedRoutes: PrerenderedRoutesRecord = {}; + + const { + baseHref = '/', + serviceWorker, + ssrOptions, + indexHtmlOptions, + optimizationOptions, + sourcemapOptions, + outputMode, + serverEntryPoint, + prerenderOptions, + appShellOptions, + publicPath, + workspaceRoot, + partialSSRBuild, + } = options; + + // Index HTML content without CSS inlining to be used for server rendering (AppShell, SSG and SSR). + // NOTE: Critical CSS inlining is deliberately omitted here, as it will be handled during server rendering. + // Additionally, when using prerendering or AppShell, the index HTML file may be regenerated. + // To prevent generating duplicate files with the same filename, a `Map` is used to store and manage the files. + const additionalHtmlOutputFiles = new Map(); + + // Generate index HTML file + // If localization is enabled, index generation is handled in the inlining process. + if (indexHtmlOptions) { + const { csrContent, ssrContent, errors, warnings } = await generateIndexHtml( + initialFiles, + outputFiles, + options, + locale, + ); + + allErrors.push(...errors); + allWarnings.push(...warnings); + + additionalHtmlOutputFiles.set( + indexHtmlOptions.output, + createOutputFile(indexHtmlOptions.output, csrContent, BuildOutputFileType.Browser), + ); + + if (ssrContent) { + additionalHtmlOutputFiles.set( + INDEX_HTML_SERVER, + createOutputFile(INDEX_HTML_SERVER, ssrContent, BuildOutputFileType.ServerApplication), + ); + } + } + + // Create server manifest + const initialFilesPaths = new Set(initialFiles.keys()); + if (serverEntryPoint && (outputMode || prerenderOptions || appShellOptions || ssrOptions)) { + const { manifestContent, serverAssetsChunks } = generateAngularServerAppManifest( + additionalHtmlOutputFiles, + outputFiles, + optimizationOptions.styles.inlineCritical ?? false, + undefined, + locale, + baseHref, + initialFilesPaths, + metafile, + publicPath, + ); + + additionalOutputFiles.push( + ...serverAssetsChunks, + createOutputFile( + SERVER_APP_MANIFEST_FILENAME, + manifestContent, + BuildOutputFileType.ServerApplication, + ), + ); + } + + // Pre-render (SSG) and App-shell + // If localization is enabled, prerendering is handled in the inlining process. + if ( + !partialSSRBuild && + (prerenderOptions || appShellOptions || (outputMode && serverEntryPoint)) && + !allErrors.length + ) { + assert( + indexHtmlOptions, + 'The "index" option is required when using the "ssg" or "appShell" options.', + ); + + const { output, warnings, errors, serializableRouteTreeNode } = await prerenderPages( + workspaceRoot, + baseHref, + appShellOptions, + prerenderOptions, + [...outputFiles, ...additionalOutputFiles], + assetFiles, + outputMode, + sourcemapOptions.scripts, + maxWorkers, + ); + + allErrors.push(...errors); + allWarnings.push(...warnings); + + const indexHasBeenPrerendered = output[indexHtmlOptions.output]; + for (const [path, { content, appShellRoute }] of Object.entries(output)) { + // Update the index contents with the app shell under these conditions: + // - Replace 'index.html' with the app shell only if it hasn't been prerendered yet. + // - Always replace 'index.csr.html' with the app shell. + let filePath = path; + if (appShellRoute && !indexHasBeenPrerendered) { + if (outputMode !== OutputMode.Server && indexHtmlOptions.output === INDEX_HTML_CSR) { + filePath = 'index.html'; + } else { + filePath = indexHtmlOptions.output; + } + } + + additionalHtmlOutputFiles.set( + filePath, + createOutputFile(filePath, content, BuildOutputFileType.Browser), + ); + } + + const serializableRouteTreeNodeForManifest: WritableSerializableRouteTreeNode = []; + for (const metadata of serializableRouteTreeNode) { + serializableRouteTreeNodeForManifest.push(metadata); + + if (metadata.renderMode === RouteRenderMode.Prerender && !metadata.route.includes('*')) { + prerenderedRoutes[metadata.route] = { headers: metadata.headers }; + } + } + + if (outputMode === OutputMode.Server) { + // Regenerate the manifest to append route tree. This is only needed if SSR is enabled. + const manifest = additionalOutputFiles.find((f) => f.path === SERVER_APP_MANIFEST_FILENAME); + assert(manifest, `${SERVER_APP_MANIFEST_FILENAME} was not found in output files.`); + + const { manifestContent, serverAssetsChunks } = generateAngularServerAppManifest( + additionalHtmlOutputFiles, + outputFiles, + optimizationOptions.styles.inlineCritical ?? false, + serializableRouteTreeNodeForManifest, + locale, + baseHref, + initialFilesPaths, + metafile, + publicPath, + ); + + for (const chunk of serverAssetsChunks) { + const idx = additionalOutputFiles.findIndex(({ path }) => path === chunk.path); + if (idx === -1) { + additionalOutputFiles.push(chunk); + } else { + additionalOutputFiles[idx] = chunk; + } + } + + manifest.contents = new TextEncoder().encode(manifestContent); + } + } + + additionalOutputFiles.push(...additionalHtmlOutputFiles.values()); + + // Augment the application with service worker support + // If localization is enabled, service worker is handled in the inlining process. + if (serviceWorker) { + try { + const serviceWorkerResult = await augmentAppWithServiceWorkerEsbuild( + workspaceRoot, + serviceWorker, + baseHref, + options.indexHtmlOptions?.output, + // Ensure additional files recently added are used + [...outputFiles, ...additionalOutputFiles], + assetFiles, + ); + + additionalOutputFiles.push( + createOutputFile('ngsw.json', serviceWorkerResult.manifest, BuildOutputFileType.Browser), + ); + additionalAssets.push(...serviceWorkerResult.assetFiles); + } catch (error) { + allErrors.push(error instanceof Error ? error.message : `${error}`); + } + } + + return { + errors: allErrors, + warnings: allWarnings, + additionalAssets, + prerenderedRoutes, + additionalOutputFiles, + }; +} diff --git a/packages/angular/build/src/builders/application/i18n.ts b/packages/angular/build/src/builders/application/i18n.ts new file mode 100644 index 000000000000..478a8893ca10 --- /dev/null +++ b/packages/angular/build/src/builders/application/i18n.ts @@ -0,0 +1,185 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 type { Metafile } from 'esbuild'; +import { join } from 'node:path'; +import { BuildOutputFileType, InitialFileRecord } from '../../tools/esbuild/bundler-context'; +import { + ExecutionResult, + PrerenderedRoutesRecord, +} from '../../tools/esbuild/bundler-execution-result'; +import { I18nInliner } from '../../tools/esbuild/i18n-inliner'; +import { maxWorkers } from '../../utils/environment-options'; +import { loadTranslations } from '../../utils/i18n-options'; +import { createTranslationLoader } from '../../utils/load-translations'; +import { executePostBundleSteps } from './execute-post-bundle'; +import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options'; + +/** + * Inlines all active locales as specified by the application build options into all + * application JavaScript files created during the build. + * @param metafile An esbuild metafile object. + * @param options The normalized application builder options used to create the build. + * @param executionResult The result of an executed build. + * @param initialFiles A map containing initial file information for the executed build. + */ +export async function inlineI18n( + metafile: Metafile, + options: NormalizedApplicationBuildOptions, + executionResult: ExecutionResult, + initialFiles: Map, +): Promise<{ + errors: string[]; + warnings: string[]; + prerenderedRoutes: PrerenderedRoutesRecord; +}> { + const { i18nOptions, optimizationOptions, baseHref, cacheOptions } = options; + + // Create the multi-threaded inliner with common options and the files generated from the build. + const inliner = new I18nInliner( + { + missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning', + outputFiles: executionResult.outputFiles, + shouldOptimize: optimizationOptions.scripts, + persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined, + }, + maxWorkers, + ); + + const inlineResult: { + errors: string[]; + warnings: string[]; + prerenderedRoutes: PrerenderedRoutesRecord; + } = { + errors: [], + warnings: [], + prerenderedRoutes: {}, + }; + + // For each active locale, use the inliner to process the output files of the build. + const updatedOutputFiles = []; + const updatedAssetFiles = []; + // Root and SSR entry files are not modified. + const unModifiedOutputFiles = executionResult.outputFiles.filter( + ({ type }) => type === BuildOutputFileType.Root || type === BuildOutputFileType.ServerRoot, + ); + + try { + for (const locale of i18nOptions.inlineLocales) { + // A locale specific set of files is returned from the inliner. + const localeInlineResult = await inliner.inlineForLocale( + locale, + i18nOptions.locales[locale].translation, + ); + const localeOutputFiles = localeInlineResult.outputFiles; + inlineResult.errors.push(...localeInlineResult.errors); + inlineResult.warnings.push(...localeInlineResult.warnings); + + const { + errors, + warnings, + additionalAssets, + additionalOutputFiles, + prerenderedRoutes: generatedRoutes, + } = await executePostBundleSteps( + metafile, + { + ...options, + baseHref: getLocaleBaseHref(baseHref, i18nOptions, locale) ?? baseHref, + }, + [...unModifiedOutputFiles, ...localeOutputFiles], + executionResult.assetFiles, + initialFiles, + locale, + ); + + localeOutputFiles.push(...additionalOutputFiles); + inlineResult.errors.push(...errors); + inlineResult.warnings.push(...warnings); + + // Update directory with locale base or subPath + const subPath = i18nOptions.locales[locale].subPath; + if (i18nOptions.flatOutput !== true) { + localeOutputFiles.forEach((file) => { + file.path = join(subPath, file.path); + }); + + for (const assetFile of [...executionResult.assetFiles, ...additionalAssets]) { + updatedAssetFiles.push({ + source: assetFile.source, + destination: join(subPath, assetFile.destination), + }); + } + } else { + executionResult.assetFiles.push(...additionalAssets); + } + + inlineResult.prerenderedRoutes = { ...inlineResult.prerenderedRoutes, ...generatedRoutes }; + updatedOutputFiles.push(...localeOutputFiles); + } + } finally { + await inliner.close(); + } + + // Update the result with all localized files. + executionResult.outputFiles = [ + // Root and SSR entry files are not modified. + ...unModifiedOutputFiles, + // Updated files for each locale. + ...updatedOutputFiles, + ]; + + // Assets are only changed if not using the flat output option + if (!i18nOptions.flatOutput) { + executionResult.assetFiles = updatedAssetFiles; + } + + return inlineResult; +} + +/** + * Loads all active translations using the translation loaders from the `@angular/localize` package. + * @param context The architect builder context for the current build. + * @param i18n The normalized i18n options to use. + */ +export async function loadActiveTranslations( + context: BuilderContext, + i18n: NormalizedApplicationBuildOptions['i18nOptions'], +) { + // Load locale data and translations (if present) + let loader; + for (const [locale, desc] of Object.entries(i18n.locales)) { + if (!i18n.inlineLocales.has(locale) && locale !== i18n.sourceLocale) { + continue; + } + + if (!desc.files.length) { + continue; + } + + loader ??= await createTranslationLoader(); + + loadTranslations( + locale, + desc, + context.workspaceRoot, + loader, + { + warn(message) { + context.logger.warn(message); + }, + error(message) { + throw new Error(message); + }, + }, + undefined, + i18n.duplicateTranslationBehavior, + ); + } +} diff --git a/packages/angular/build/src/builders/application/index.ts b/packages/angular/build/src/builders/application/index.ts new file mode 100644 index 000000000000..80261c41277f --- /dev/null +++ b/packages/angular/build/src/builders/application/index.ts @@ -0,0 +1,276 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 { Builder, BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; +import assert from 'node:assert'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { BuildOutputFileType } from '../../tools/esbuild/bundler-context'; +import { createJsonBuildManifest, emitFilesToDisk } from '../../tools/esbuild/utils'; +import { colors as ansiColors } from '../../utils/color'; +import { deleteOutputDir } from '../../utils/delete-output-dir'; +import { useJSONBuildLogs } from '../../utils/environment-options'; +import { purgeStaleBuildCache } from '../../utils/purge-cache'; +import { assertCompatibleAngularVersion } from '../../utils/version'; +import { runEsBuildBuildAction } from './build-action'; +import { executeBuild } from './execute-build'; +import { + ApplicationBuilderExtensions, + ApplicationBuilderInternalOptions, + NormalizedOutputOptions, + normalizeOptions, +} from './options'; +import { Result, ResultKind } from './results'; +import { Schema as ApplicationBuilderOptions } from './schema'; + +const isNodeV22orHigher = Number(process.versions.node.split('.', 1)[0]) >= 22; + +export type { ApplicationBuilderOptions }; + +export async function* buildApplicationInternal( + options: ApplicationBuilderInternalOptions, + // TODO: Integrate abort signal support into builder system + context: BuilderContext & { signal?: AbortSignal }, + extensions?: ApplicationBuilderExtensions, +): AsyncIterable { + const { workspaceRoot, logger, target } = context; + + // Check Angular version. + assertCompatibleAngularVersion(workspaceRoot); + + // Purge old build disk cache. + await purgeStaleBuildCache(context); + + // Determine project name from builder context target + const projectName = target?.project; + if (!projectName) { + context.logger.error(`The 'application' builder requires a target to be specified.`); + // Only the vite-based dev server current uses the errors value + yield { kind: ResultKind.Failure, errors: [] }; + + return; + } + + const normalizedOptions = await normalizeOptions(context, projectName, options, extensions); + + if (!normalizedOptions.outputOptions.ignoreServer) { + const { browser, server } = normalizedOptions.outputOptions; + if (browser === '') { + context.logger.error( + `'outputPath.browser' cannot be configured to an empty string when SSR is enabled.`, + ); + yield { kind: ResultKind.Failure, errors: [] }; + + return; + } + + if (browser === server) { + context.logger.error( + `'outputPath.browser' and 'outputPath.server' cannot be configured to the same value.`, + ); + yield { kind: ResultKind.Failure, errors: [] }; + + return; + } + } + + // Setup an abort controller with a builder teardown if no signal is present + let signal = context.signal; + if (!signal) { + const controller = new AbortController(); + signal = controller.signal; + context.addTeardown(() => controller.abort('builder-teardown')); + } + + yield* runEsBuildBuildAction( + async (rebuildState) => { + const { serverEntryPoint, jsonLogs, partialSSRBuild } = normalizedOptions; + + const startTime = process.hrtime.bigint(); + const result = await executeBuild(normalizedOptions, context, rebuildState); + + if (jsonLogs) { + result.addLog(await createJsonBuildManifest(result, normalizedOptions)); + } else { + if (serverEntryPoint && !partialSSRBuild) { + const prerenderedRoutesLength = Object.keys(result.prerenderedRoutes).length; + let prerenderMsg = `Prerendered ${prerenderedRoutesLength} static route`; + prerenderMsg += prerenderedRoutesLength !== 1 ? 's.' : '.'; + + result.addLog(ansiColors.magenta(prerenderMsg)); + } + + const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9; + const hasError = result.errors.length > 0; + + result.addLog( + `Application bundle generation ${hasError ? 'failed' : 'complete'}. [${buildTime.toFixed(3)} seconds]\n`, + ); + } + + return result; + }, + { + watch: normalizedOptions.watch, + preserveSymlinks: normalizedOptions.preserveSymlinks, + poll: normalizedOptions.poll, + cacheOptions: normalizedOptions.cacheOptions, + outputOptions: normalizedOptions.outputOptions, + verbose: normalizedOptions.verbose, + projectRoot: normalizedOptions.projectRoot, + workspaceRoot: normalizedOptions.workspaceRoot, + progress: normalizedOptions.progress, + clearScreen: normalizedOptions.clearScreen, + colors: normalizedOptions.colors, + jsonLogs: normalizedOptions.jsonLogs, + incrementalResults: normalizedOptions.incrementalResults, + logger, + signal, + }, + ); +} + +/** + * Builds an application using the `application` builder with the provided + * options. + * + * Usage of the `extensions` parameter is NOT supported and may cause unexpected + * build output or build failures. + * + * @experimental Direct usage of this function is considered experimental. + * + * @param options The options defined by the builder's schema to use. + * @param context An Architect builder context instance. + * @param extensions An object contain extension points for the build. + * @returns The build output results of the build. + */ +export async function* buildApplication( + options: ApplicationBuilderOptions, + context: BuilderContext, + extensions?: ApplicationBuilderExtensions, +): AsyncIterable { + let initial = true; + const internalOptions = { ...options, incrementalResults: true }; + for await (const result of buildApplicationInternal(internalOptions, context, extensions)) { + const outputOptions = result.detail?.['outputOptions'] as NormalizedOutputOptions | undefined; + + if (initial) { + initial = false; + + // Clean the output location if requested. + // Output options may not be present if the build failed. + if (outputOptions?.clean) { + await deleteOutputDir(context.workspaceRoot, outputOptions.base, [ + outputOptions.browser, + outputOptions.server, + ]); + } + } + + if (result.kind === ResultKind.Failure) { + yield { success: false }; + continue; + } + + assert(outputOptions, 'Application output options are required for builder usage.'); + assert( + result.kind === ResultKind.Full || result.kind === ResultKind.Incremental, + 'Application build did not provide a file result output.', + ); + + // TODO: Restructure output logging to better handle stdout JSON piping + if (!useJSONBuildLogs) { + context.logger.info(`Output location: ${outputOptions.base}\n`); + } + + // Writes the output files to disk and ensures the containing directories are present + const directoryExists = new Set(); + await emitFilesToDisk(Object.entries(result.files), async ([filePath, file]) => { + if ( + outputOptions.ignoreServer && + (file.type === BuildOutputFileType.ServerApplication || + file.type === BuildOutputFileType.ServerRoot) + ) { + return; + } + + const fullFilePath = generateFullPath(filePath, file.type, outputOptions); + + // Ensure output subdirectories exist + const fileBasePath = path.dirname(fullFilePath); + if (fileBasePath && !directoryExists.has(fileBasePath)) { + await fs.mkdir(fileBasePath, { recursive: true }); + directoryExists.add(fileBasePath); + } + + if (file.origin === 'memory') { + // Write file contents + await fs.writeFile(fullFilePath, file.contents); + } else { + // Copy file contents + if (isNodeV22orHigher) { + // Use newer `cp` API on Node.js 22+ (minimum v22 for CLI is 22.11) + await fs.cp(file.inputPath, fullFilePath, { + mode: fs.constants.COPYFILE_FICLONE, + preserveTimestamps: true, + }); + } else { + // For Node.js 20 use `copyFile` (`cp` is not stable for v20) + // TODO: Remove when Node.js 20 is no longer supported + await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE); + } + } + }); + + // Delete any removed files if incremental + if (result.kind === ResultKind.Incremental && result.removed?.length) { + await Promise.all( + result.removed.map((file) => { + const fullFilePath = generateFullPath(file.path, file.type, outputOptions); + + return fs.rm(fullFilePath, { force: true, maxRetries: 3 }); + }), + ); + } + + yield { success: true }; + } +} + +function generateFullPath( + filePath: string, + type: BuildOutputFileType, + outputOptions: NormalizedOutputOptions, +) { + let typeDirectory: string; + switch (type) { + case BuildOutputFileType.Browser: + case BuildOutputFileType.Media: + typeDirectory = outputOptions.browser; + break; + case BuildOutputFileType.ServerApplication: + case BuildOutputFileType.ServerRoot: + typeDirectory = outputOptions.server; + break; + case BuildOutputFileType.Root: + typeDirectory = ''; + break; + default: + throw new Error( + `Unhandled write for file "${filePath}" with type "${BuildOutputFileType[type]}".`, + ); + } + // NOTE: 'base' is a fully resolved path at this point + const fullFilePath = path.join(outputOptions.base, typeDirectory, filePath); + + return fullFilePath; +} + +const builder: Builder = createBuilder(buildApplication); + +export default builder; diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts new file mode 100644 index 000000000000..60b240eb9d74 --- /dev/null +++ b/packages/angular/build/src/builders/application/options.ts @@ -0,0 +1,718 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { BuilderContext } from '@angular-devkit/architect'; +import type { Plugin } from 'esbuild'; +import { realpathSync } from 'node:fs'; +import { access, constants, readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } from '../../utils'; +import { supportColor } from '../../utils/color'; +import { useJSONBuildLogs, usePartialSsrBuild } from '../../utils/environment-options'; +import { I18nOptions, createI18nOptions } from '../../utils/i18n-options'; +import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator'; +import { normalizeCacheOptions } from '../../utils/normalize-cache'; +import { + SearchDirectory, + findTailwindConfiguration, + generateSearchDirectories, + loadPostcssConfiguration, +} from '../../utils/postcss-configuration'; +import { getProjectRootPaths, normalizeDirectoryPath } from '../../utils/project-metadata'; +import { urlJoin } from '../../utils/url'; +import { + Schema as ApplicationBuilderOptions, + ExperimentalPlatform, + I18NTranslation, + OutputHashing, + OutputMode, + OutputPathClass, +} from './schema'; + +/** + * The filename for the client-side rendered HTML template. + * This template is used for client-side rendering (CSR) in a web application. + */ +export const INDEX_HTML_CSR = 'index.csr.html'; + +/** + * The filename for the server-side rendered HTML template. + * This template is used for server-side rendering (SSR) in a web application. + */ +export const INDEX_HTML_SERVER = 'index.server.html'; + +export type NormalizedOutputOptions = Required & { + clean: boolean; + ignoreServer: boolean; +}; +export type NormalizedApplicationBuildOptions = Awaited>; + +export interface ApplicationBuilderExtensions { + codePlugins?: Plugin[]; + indexHtmlTransformer?: IndexHtmlTransform; +} + +/** Internal options hidden from builder schema but available when invoked programmatically. */ +interface InternalOptions { + /** + * Entry points to use for the compilation. Incompatible with `browser`, which must not be provided. May be relative or absolute paths. + * If given a relative path, it is resolved relative to the current workspace and will generate an output at the same relative location + * in the output directory. If given an absolute path, the output will be generated in the root of the output directory with the same base + * name. + * + * If provided a Map, the key is the name of the output bundle and the value is the entry point file. + */ + entryPoints?: Set | Map; + + /** File extension to use for the generated output files. */ + outExtension?: 'js' | 'mjs'; + + /** + * Indicates whether all node packages should be marked as external. + * Currently used by the dev-server to support prebundling. + */ + externalPackages?: boolean | { exclude: string[] }; + + /** + * Forces the output from the localize post-processing to not create nested directories per locale output. + * This is only used by the development server which currently only supports a single locale per build. + */ + forceI18nFlatOutput?: boolean; + + /** + * When set to `true`, enables fast SSR in development mode by disabling the full manifest generation and prerendering. + * + * This option is intended to optimize performance during development by skipping prerendering and route extraction when not required. + * @default false + */ + partialSSRBuild?: boolean; + + /** + * Enables the use of AOT compiler emitted external runtime styles. + * External runtime styles use `link` elements instead of embedded style content in the output JavaScript. + * This option is only intended to be used with a development server that can process and serve component + * styles. + */ + externalRuntimeStyles?: boolean; + + /** + * Enables the AOT compiler to generate template component update functions. + * This option is only intended to be used with a development server that can process and serve component + * template updates. + */ + templateUpdates?: boolean; + + /** + * Enables emitting incremental build results when in watch mode. A full build result will only be emitted + * for the initial build. This option also requires watch to be enabled to have an effect. + */ + incrementalResults?: boolean; + + /** + * Enables instrumentation to collect code coverage data for specific files. + * + * Used exclusively for tests and shouldn't be used for other kinds of builds. + */ + instrumentForCoverage?: (filename: string) => boolean; +} + +/** Full set of options for `application` builder. */ +export type ApplicationBuilderInternalOptions = Omit< + ApplicationBuilderOptions & InternalOptions, + 'browser' +> & { + // `browser` can be `undefined` if `entryPoints` is used. + browser?: string; +}; + +/** + * Normalize the user provided options by creating full paths for all path based options + * and converting multi-form options into a single form that can be directly used + * by the build process. + * + * @param context The context for current builder execution. + * @param projectName The name of the project for the current execution. + * @param options An object containing the options to use for the build. + * @param plugins An optional array of programmatically supplied build plugins. + * @returns An object containing normalized options required to perform the build. + */ +// eslint-disable-next-line max-lines-per-function +export async function normalizeOptions( + context: BuilderContext, + projectName: string, + options: ApplicationBuilderInternalOptions, + extensions?: ApplicationBuilderExtensions, +) { + // If not explicitly set, default to the Node.js process argument + const preserveSymlinks = + options.preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks'); + + // Setup base paths based on workspace root and project information + const workspaceRoot = preserveSymlinks + ? context.workspaceRoot + : // NOTE: promises.realpath should not be used here since it uses realpath.native which + // can cause case conversion and other undesirable behavior on Windows systems. + // ref: https://github.com/nodejs/node/issues/7726 + realpathSync(context.workspaceRoot); + const projectMetadata = await context.getProjectMetadata(projectName); + const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata); + + // Gather persistent caching option and provide a project specific cache location + const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot); + cacheOptions.path = path.join(cacheOptions.path, projectName); + + const i18nOptions: I18nOptions & { + duplicateTranslationBehavior?: I18NTranslation; + missingTranslationBehavior?: I18NTranslation; + } = createI18nOptions(projectMetadata, options.localize, context.logger, !!options.ssr); + i18nOptions.duplicateTranslationBehavior = options.i18nDuplicateTranslation; + i18nOptions.missingTranslationBehavior = options.i18nMissingTranslation; + if (options.forceI18nFlatOutput) { + i18nOptions.flatOutput = true; + } + + const entryPoints = normalizeEntryPoints(workspaceRoot, options.browser, options.entryPoints); + const tsconfig = path.join(workspaceRoot, options.tsConfig); + const optimizationOptions = normalizeOptimization(options.optimization); + const sourcemapOptions = normalizeSourceMaps(options.sourceMap ?? false); + const assets = options.assets?.length + ? normalizeAssetPatterns(options.assets, workspaceRoot, projectRoot, projectSourceRoot) + : undefined; + + let fileReplacements: Record | undefined; + if (options.fileReplacements) { + for (const replacement of options.fileReplacements) { + const fileReplaceWith = path.join(workspaceRoot, replacement.with); + + try { + await access(fileReplaceWith, constants.F_OK); + } catch { + throw new Error(`The ${fileReplaceWith} path in file replacements does not exist.`); + } + + fileReplacements ??= {}; + fileReplacements[path.join(workspaceRoot, replacement.replace)] = fileReplaceWith; + } + } + + let loaderExtensions: Record | undefined; + if (options.loader) { + for (const [extension, value] of Object.entries(options.loader)) { + if (extension[0] !== '.' || /\.[cm]?[jt]sx?$/.test(extension)) { + continue; + } + if (value !== 'text' && value !== 'binary' && value !== 'file' && value !== 'empty') { + continue; + } + loaderExtensions ??= {}; + loaderExtensions[extension] = value; + } + } + + // Validate prerender and ssr options when using the outputMode + if (options.outputMode === OutputMode.Server) { + if (!options.server) { + throw new Error('The "server" option is required when "outputMode" is set to "server".'); + } + + if (typeof options.ssr === 'boolean' || !options.ssr?.entry) { + throw new Error('The "ssr.entry" option is required when "outputMode" is set to "server".'); + } + } + + if (options.outputMode) { + if (!options.server) { + options.ssr = false; + } + + if (options.prerender !== undefined) { + context.logger.warn( + 'The "prerender" option is not considered when "outputMode" is specified.', + ); + } + + options.prerender = !!options.server; + + if (options.appShell !== undefined) { + context.logger.warn( + 'The "appShell" option is not considered when "outputMode" is specified.', + ); + } + } + + // A configuration file can exist in the project or workspace root + const searchDirectories = await generateSearchDirectories([projectRoot, workspaceRoot]); + const postcssConfiguration = await loadPostcssConfiguration(searchDirectories); + // Skip tailwind configuration if postcss is customized + const tailwindConfiguration = postcssConfiguration + ? undefined + : await getTailwindConfig(searchDirectories, workspaceRoot, context); + + let serverEntryPoint: string | undefined; + if (typeof options.server === 'string') { + if (options.server === '') { + throw new Error('The "server" option cannot be an empty string.'); + } + + serverEntryPoint = path.join(workspaceRoot, options.server); + } + + let prerenderOptions; + if (options.prerender) { + const { discoverRoutes = true, routesFile = undefined } = + options.prerender === true ? {} : options.prerender; + + prerenderOptions = { + discoverRoutes, + routesFile: routesFile && path.join(workspaceRoot, routesFile), + }; + } + + let ssrOptions; + if (options.ssr === true) { + ssrOptions = {}; + } else if (typeof options.ssr === 'object') { + const { entry, experimentalPlatform = ExperimentalPlatform.Node } = options.ssr; + + ssrOptions = { + entry: entry && path.join(workspaceRoot, entry), + platform: experimentalPlatform, + }; + } + + let appShellOptions; + if (options.appShell) { + appShellOptions = { + route: 'shell', + }; + } + + const outputPath = options.outputPath ?? path.join(workspaceRoot, 'dist', projectName); + const outputOptions: NormalizedOutputOptions = { + browser: 'browser', + server: 'server', + media: 'media', + ...(typeof outputPath === 'string' ? undefined : outputPath), + base: normalizeDirectoryPath( + path.resolve(workspaceRoot, typeof outputPath === 'string' ? outputPath : outputPath.base), + ), + clean: options.deleteOutputPath ?? true, + // For app-shell and SSG server files are not required by users. + // Omit these when SSR is not enabled. + ignoreServer: + ((ssrOptions === undefined || serverEntryPoint === undefined) && + options.outputMode === undefined) || + options.outputMode === OutputMode.Static, + }; + + const outputNames = { + bundles: + options.outputHashing === OutputHashing.All || options.outputHashing === OutputHashing.Bundles + ? '[name]-[hash]' + : '[name]', + media: + outputOptions.media + + (options.outputHashing === OutputHashing.All || options.outputHashing === OutputHashing.Media + ? '/[name]-[hash]' + : '/[name]'), + }; + + const globalStyles = normalizeGlobalEntries(options.styles, 'styles'); + const globalScripts = normalizeGlobalEntries(options.scripts, 'scripts'); + let indexHtmlOptions; + // index can never have a value of `true` but in the schema it's of type `boolean`. + if (typeof options.index !== 'boolean') { + let indexInput: string; + let indexOutput: string; + // The output file will be created within the configured output path + if (typeof options.index === 'string') { + indexInput = indexOutput = path.join(workspaceRoot, options.index); + } else if (typeof options.index === 'undefined') { + indexInput = path.join(projectSourceRoot, 'index.html'); + indexOutput = 'index.html'; + } else { + indexInput = path.join(workspaceRoot, options.index.input); + indexOutput = options.index.output || 'index.html'; + } + + /** + * If SSR is activated, create a distinct entry file for the `index.html`. + * This is necessary because numerous server/cloud providers automatically serve the `index.html` as a static file + * if it exists (handling SSG). + * + * For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server. + * + * This approach can also be applied to service workers, where the `index.csr.html` is served instead of the prerendered `index.html`. + */ + const indexBaseName = path.basename(indexOutput); + indexOutput = + (ssrOptions || prerenderOptions) && indexBaseName === 'index.html' + ? INDEX_HTML_CSR + : indexBaseName; + + indexHtmlOptions = { + input: indexInput, + output: indexOutput, + insertionOrder: [ + ['polyfills', true], + ...globalStyles.filter((s) => s.initial).map((s) => [s.name, false]), + ...globalScripts.filter((s) => s.initial).map((s) => [s.name, false]), + ['main', true], + // [name, esm] + ] as [string, boolean][], + transformer: extensions?.indexHtmlTransformer, + // Preload initial defaults to true + preloadInitial: typeof options.index !== 'object' || (options.index.preloadInitial ?? true), + }; + } + + if (appShellOptions || ssrOptions || prerenderOptions) { + if (!serverEntryPoint) { + throw new Error( + 'The "server" option is required when enabling "ssr", "prerender" or "app-shell".', + ); + } + + if (!indexHtmlOptions) { + throw new Error( + 'The "index" option cannot be set to false when enabling "ssr", "prerender" or "app-shell".', + ); + } + } + + const autoCsp = options.security?.autoCsp; + const security = { + autoCsp: autoCsp + ? { + unsafeEval: autoCsp === true ? false : !!autoCsp.unsafeEval, + } + : undefined, + }; + + // Initial options to keep + const { + allowedCommonJsDependencies, + aot = true, + baseHref, + crossOrigin, + externalDependencies, + extractLicenses, + inlineStyleLanguage = 'css', + outExtension, + serviceWorker, + poll, + polyfills, + statsJson, + outputMode, + stylePreprocessorOptions, + subresourceIntegrity, + verbose, + watch, + progress = true, + externalPackages, + namedChunks, + budgets, + deployUrl, + clearScreen, + define, + partialSSRBuild = false, + externalRuntimeStyles, + instrumentForCoverage, + } = options; + + // Return all the normalized options + return { + advancedOptimizations: !!aot && optimizationOptions.scripts, + allowedCommonJsDependencies, + baseHref, + cacheOptions, + crossOrigin, + externalDependencies: normalizeExternals(externalDependencies), + externalPackages: + typeof externalPackages === 'object' + ? { + ...externalPackages, + exclude: normalizeExternals(externalPackages.exclude), + } + : externalPackages, + extractLicenses, + inlineStyleLanguage, + jit: !aot, + stats: !!statsJson, + polyfills: polyfills === undefined || Array.isArray(polyfills) ? polyfills : [polyfills], + poll, + progress, + preserveSymlinks, + stylePreprocessorOptions, + subresourceIntegrity, + serverEntryPoint, + prerenderOptions, + appShellOptions, + outputMode, + ssrOptions, + verbose, + watch, + workspaceRoot, + entryPoints, + optimizationOptions, + outputOptions, + outExtension, + sourcemapOptions, + tsconfig, + projectRoot, + assets, + outputNames, + fileReplacements, + globalStyles, + globalScripts, + serviceWorker: serviceWorker + ? path.join( + workspaceRoot, + typeof serviceWorker === 'string' ? serviceWorker : 'src/ngsw-config.json', + ) + : undefined, + indexHtmlOptions, + tailwindConfiguration, + postcssConfiguration, + i18nOptions, + namedChunks, + budgets: budgets?.length ? budgets : undefined, + publicPath: deployUrl, + plugins: extensions?.codePlugins?.length ? extensions?.codePlugins : undefined, + loaderExtensions, + jsonLogs: useJSONBuildLogs, + colors: supportColor(), + clearScreen, + define, + partialSSRBuild: usePartialSsrBuild || partialSSRBuild, + externalRuntimeStyles: aot && externalRuntimeStyles, + instrumentForCoverage, + security, + templateUpdates: !!options.templateUpdates, + incrementalResults: !!options.incrementalResults, + customConditions: options.conditions, + frameworkVersion: await findFrameworkVersion(projectRoot), + }; +} + +async function getTailwindConfig( + searchDirectories: SearchDirectory[], + workspaceRoot: string, + context: BuilderContext, +): Promise<{ file: string; package: string } | undefined> { + const tailwindConfigurationPath = findTailwindConfiguration(searchDirectories); + + if (!tailwindConfigurationPath) { + return undefined; + } + + // Create a node resolver from the configuration file + const resolver = createRequire(tailwindConfigurationPath); + try { + return { + file: tailwindConfigurationPath, + package: resolver.resolve('tailwindcss'), + }; + } catch { + const relativeTailwindConfigPath = path.relative(workspaceRoot, tailwindConfigurationPath); + context.logger.warn( + `Tailwind CSS configuration file found (${relativeTailwindConfigPath})` + + ` but the 'tailwindcss' package is not installed.` + + ` To enable Tailwind CSS, please install the 'tailwindcss' package.`, + ); + } + + return undefined; +} + +/** + * Normalize entry point options. To maintain compatibility with the legacy browser builder, we need a single `browser` + * option which defines a single entry point. However, we also want to support multiple entry points as an internal option. + * The two options are mutually exclusive and if `browser` is provided it will be used as the sole entry point. + * If `entryPoints` are provided, they will be used as the set of entry points. + * + * @param workspaceRoot Path to the root of the Angular workspace. + * @param browser The `browser` option pointing at the application entry point. While required per the schema file, it may be omitted by + * programmatic usages of `browser-esbuild`. + * @param entryPoints Set of entry points to use if provided. + * @returns An object mapping entry point names to their file paths. + */ +function normalizeEntryPoints( + workspaceRoot: string, + browser: string | undefined, + entryPoints: Set | Map = new Set(), +): Record { + if (browser === '') { + throw new Error('`browser` option cannot be an empty string.'); + } + + // `browser` and `entryPoints` are mutually exclusive. + if (browser && entryPoints.size > 0) { + throw new Error('Only one of `browser` or `entryPoints` may be provided.'); + } + if (!browser && entryPoints.size === 0) { + // Schema should normally reject this case, but programmatic usages of the builder might make this mistake. + throw new Error('Either `browser` or at least one `entryPoints` value must be provided.'); + } + + // Schema types force `browser` to always be provided, but it may be omitted when the builder is invoked programmatically. + if (browser) { + // Use `browser` alone. + return { 'main': path.join(workspaceRoot, browser) }; + } else if (entryPoints instanceof Map) { + return Object.fromEntries( + Array.from(entryPoints.entries(), ([name, entryPoint]) => { + // Get the full file path to a relative entry point input. Leave bare specifiers alone so they are resolved as modules. + const isRelativePath = entryPoint.startsWith('.'); + const entryPointPath = isRelativePath ? path.join(workspaceRoot, entryPoint) : entryPoint; + + return [name, entryPointPath]; + }), + ); + } else { + // Use `entryPoints` alone. + const entryPointPaths: Record = {}; + for (const entryPoint of entryPoints) { + const parsedEntryPoint = path.parse(entryPoint); + + // Use the input file path without an extension as the "name" of the entry point dictating its output location. + // Relative entry points are generated at the same relative path in the output directory. + // Absolute entry points are always generated with the same file name in the root of the output directory. This includes absolute + // paths pointing at files actually within the workspace root. + const entryPointName = path.isAbsolute(entryPoint) + ? parsedEntryPoint.name + : path.join(parsedEntryPoint.dir, parsedEntryPoint.name); + + // Get the full file path to a relative entry point input. Leave bare specifiers alone so they are resolved as modules. + const isRelativePath = entryPoint.startsWith('.'); + const entryPointPath = isRelativePath ? path.join(workspaceRoot, entryPoint) : entryPoint; + + // Check for conflicts with previous entry points. + const existingEntryPointPath = entryPointPaths[entryPointName]; + if (existingEntryPointPath) { + throw new Error( + `\`${existingEntryPointPath}\` and \`${entryPointPath}\` both output to the same location \`${entryPointName}\`.` + + ' Rename or move one of the files to fix the conflict.', + ); + } + + entryPointPaths[entryPointName] = entryPointPath; + } + + return entryPointPaths; + } +} + +function normalizeGlobalEntries( + rawEntries: ({ bundleName?: string; input: string; inject?: boolean } | string)[] | undefined, + defaultName: string, +): { name: string; files: string[]; initial: boolean }[] { + if (!rawEntries?.length) { + return []; + } + + const bundles = new Map(); + + for (const rawEntry of rawEntries) { + let entry; + if (typeof rawEntry === 'string') { + // string entries use default bundle name and inject values + entry = { input: rawEntry }; + } else { + entry = rawEntry; + } + + const { bundleName, input, inject = true } = entry; + + // Non-injected entries default to the file name + const name = bundleName || (inject ? defaultName : path.basename(input, path.extname(input))); + + const existing = bundles.get(name); + if (!existing) { + bundles.set(name, { name, files: [input], initial: inject }); + continue; + } + + if (existing.initial !== inject) { + throw new Error( + `The "${name}" bundle is mixing injected and non-injected entries. ` + + 'Verify that the project options are correct.', + ); + } + + existing.files.push(input); + } + + return [...bundles.values()]; +} + +export function getLocaleBaseHref( + baseHref: string | undefined = '', + i18n: NormalizedApplicationBuildOptions['i18nOptions'], + locale: string, +): string | undefined { + if (i18n.flatOutput) { + return undefined; + } + + const localeData = i18n.locales[locale]; + if (!localeData) { + return undefined; + } + + const baseHrefSuffix = localeData.baseHref ?? localeData.subPath + '/'; + + return baseHrefSuffix !== '' ? urlJoin(baseHref, baseHrefSuffix) : undefined; +} + +/** + * Normalizes an array of external dependency paths by ensuring that + * wildcard patterns (`/*`) are removed from package names. + * + * This avoids the need to handle this normalization repeatedly in our plugins, + * as esbuild already treats `--external:@foo/bar` as implicitly including + * `--external:@foo/bar/*`. By standardizing the input, we ensure consistency + * and reduce redundant checks across our plugins. + * + * @param value - An optional array of dependency paths to normalize. + * @returns A new array with wildcard patterns removed from package names, or `undefined` if input is `undefined`. + */ +function normalizeExternals(value: string[] | undefined): string[] | undefined { + if (!value) { + return undefined; + } + + return [ + ...new Set( + value.map((d) => + // remove "/*" wildcard in the end if provided string is not path-like + d.endsWith('/*') && !/^\.{0,2}\//.test(d) ? d.slice(0, -2) : d, + ), + ), + ]; +} + +async function findFrameworkVersion(projectRoot: string): Promise { + // Create a custom require function for ESM compliance. + // NOTE: The trailing slash is significant. + const projectResolve = createRequire(projectRoot + '/').resolve; + + try { + const manifestPath = projectResolve('@angular/core/package.json'); + const manifestData = await readFile(manifestPath, 'utf-8'); + const manifestObject = JSON.parse(manifestData) as { version: string }; + const version = manifestObject.version; + + return version; + } catch { + throw new Error( + 'Error: It appears that "@angular/core" is missing as a dependency. Please ensure it is included in your project.', + ); + } +} diff --git a/packages/angular/build/src/builders/application/results.ts b/packages/angular/build/src/builders/application/results.ts new file mode 100644 index 000000000000..3e3f0dd99315 --- /dev/null +++ b/packages/angular/build/src/builders/application/results.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 + */ + +import { BuildOutputFileType } from '../../tools/esbuild/bundler-context'; + +export enum ResultKind { + Failure, + Full, + Incremental, + ComponentUpdate, +} + +export type Result = FailureResult | FullResult | IncrementalResult | ComponentUpdateResult; + +export interface BaseResult { + kind: ResultKind; + warnings?: ResultMessage[]; + duration?: number; + detail?: Record; +} + +export interface FailureResult extends BaseResult { + kind: ResultKind.Failure; + errors: ResultMessage[]; +} + +export interface FullResult extends BaseResult { + kind: ResultKind.Full; + files: Record; +} + +export interface IncrementalResult extends BaseResult { + kind: ResultKind.Incremental; + background?: boolean; + added: string[]; + removed: { path: string; type: BuildOutputFileType }[]; + modified: string[]; + files: Record; +} + +export type ResultFile = DiskFile | MemoryFile; + +export interface BaseResultFile { + origin: 'memory' | 'disk'; + type: BuildOutputFileType; +} + +export interface DiskFile extends BaseResultFile { + origin: 'disk'; + inputPath: string; +} + +export interface MemoryFile extends BaseResultFile { + origin: 'memory'; + hash: string; + contents: Uint8Array; +} + +export interface ResultMessage { + text: string; + location?: { file: string; line: number; column: number } | null; + notes?: { text: string }[]; +} + +export interface ComponentUpdateResult extends BaseResult { + kind: ResultKind.ComponentUpdate; + updates: { + id: string; + type: 'style' | 'template'; + content: string; + }[]; +} diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json new file mode 100644 index 000000000000..934bfe9390f4 --- /dev/null +++ b/packages/angular/build/src/builders/application/schema.json @@ -0,0 +1,720 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Application schema for Build Facade.", + "description": "Application builder target options", + "type": "object", + "properties": { + "assets": { + "type": "array", + "description": "List of static application assets.", + "default": [], + "items": { + "$ref": "#/definitions/assetPattern" + } + }, + "browser": { + "type": "string", + "description": "The full path for the browser entry point to the application, relative to the current workspace." + }, + "server": { + "description": "The full path for the server entry point to the application, relative to the current workspace.", + "oneOf": [ + { + "type": "string", + "description": "The full path for the server entry point to the application, relative to the current workspace." + }, + { + "const": false, + "type": "boolean", + "description": "Indicates that a server entry point is not provided." + } + ] + }, + "polyfills": { + "description": "A list of polyfills to include in the build. Can be a full path for a file, relative to the current workspace or module specifier. Example: 'zone.js'.", + "type": "array", + "items": { + "type": "string", + "uniqueItems": true + }, + "default": [] + }, + "tsConfig": { + "type": "string", + "description": "The full path for the TypeScript configuration file, relative to the current workspace." + }, + "deployUrl": { + "type": "string", + "description": "Customize the base path for the URLs of resources in 'index.html' and component stylesheets. This option is only necessary for specific deployment scenarios, such as with Angular Elements or when utilizing different CDN locations." + }, + "security": { + "description": "Security features to protect against XSS and other common attacks", + "type": "object", + "additionalProperties": false, + "properties": { + "autoCsp": { + "description": "Enables automatic generation of a hash-based Strict Content Security Policy (https://web.dev/articles/strict-csp#choose-hash) based on scripts in index.html. Will default to true once we are out of experimental/preview phases.", + "default": false, + "oneOf": [ + { + "type": "object", + "properties": { + "unsafeEval": { + "type": "boolean", + "description": "Include the `unsafe-eval` directive (https://web.dev/articles/strict-csp#remove-eval) in the auto-CSP. Please only enable this if you are absolutely sure that you need to, as allowing calls to eval will weaken the XSS defenses provided by the auto-CSP.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + } + } + }, + "scripts": { + "description": "Global scripts to be included in the build.", + "type": "array", + "default": [], + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include.", + "pattern": "\\.[cm]?jsx?$" + }, + "bundleName": { + "type": "string", + "pattern": "^[\\w\\-.]*$", + "description": "The bundle name for this extra entry point." + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true + } + }, + "additionalProperties": false, + "required": ["input"] + }, + { + "type": "string", + "description": "The JavaScript/TypeScript file or package containing the file to include." + } + ] + } + }, + "styles": { + "description": "Global styles to be included in the build.", + "type": "array", + "default": [], + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include.", + "pattern": "\\.(?:css|scss|sass|less)$" + }, + "bundleName": { + "type": "string", + "pattern": "^[\\w\\-.]*$", + "description": "The bundle name for this extra entry point." + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true + } + }, + "additionalProperties": false, + "required": ["input"] + }, + { + "type": "string", + "description": "The file to include.", + "pattern": "\\.(?:css|scss|sass|less)$" + } + ] + } + }, + "inlineStyleLanguage": { + "description": "The stylesheet language to use for the application's inline component styles.", + "type": "string", + "default": "css", + "enum": ["css", "less", "sass", "scss"] + }, + "stylePreprocessorOptions": { + "description": "Options to pass to style preprocessors.", + "type": "object", + "properties": { + "includePaths": { + "description": "Paths to include. Paths will be resolved to workspace root.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "sass": { + "description": "Options to pass to the sass preprocessor.", + "type": "object", + "properties": { + "fatalDeprecations": { + "description": "A set of deprecations to treat as fatal. If a deprecation warning of any provided type is encountered during compilation, the compiler will error instead. If a Version is provided, then all deprecations that were active in that compiler version will be treated as fatal.", + "type": "array", + "items": { + "type": "string" + } + }, + "silenceDeprecations": { + "description": " A set of active deprecations to ignore. If a deprecation warning of any provided type is encountered during compilation, the compiler will ignore it instead.", + "type": "array", + "items": { + "type": "string" + } + }, + "futureDeprecations": { + "description": "A set of future deprecations to opt into early. Future deprecations passed here will be treated as active by the compiler, emitting warnings as necessary.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "externalDependencies": { + "description": "Exclude the listed external dependencies from being bundled into the bundle. Instead, the created bundle relies on these dependencies to be available during runtime. Note: `@foo/bar` marks all paths within the `@foo/bar` package as external, including sub-paths like `@foo/bar/baz`.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "clearScreen": { + "type": "boolean", + "default": false, + "description": "Automatically clear the terminal screen during rebuilds." + }, + "optimization": { + "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For more information, see https://angular.dev/reference/configs/workspace-config#optimization-configuration.", + "default": true, + "x-user-analytics": "ep.ng_optimization", + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Enables optimization of the scripts output.", + "default": true + }, + "styles": { + "description": "Enables optimization of the styles output.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "minify": { + "type": "boolean", + "description": "Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers and minimizing values.", + "default": true + }, + "inlineCritical": { + "type": "boolean", + "description": "Extract and inline critical CSS definitions to improve first paint time.", + "default": true + }, + "removeSpecialComments": { + "type": "boolean", + "description": "Remove comments in global CSS that contains '@license' or '@preserve' or that starts with '//!' or '/*!'.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + }, + "fonts": { + "description": "Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "inline": { + "type": "boolean", + "description": "Reduce render blocking requests by inlining external Google Fonts and Adobe Fonts CSS definitions in the application's HTML index file. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + }, + "loader": { + "description": "Defines the type of loader to use with a specified file extension when used with a JavaScript `import`. `text` inlines the content as a string; `binary` inlines the content as a Uint8Array; `file` emits the file and provides the runtime location of the file; `empty` considers the content to be empty and not include it in bundles.", + "type": "object", + "patternProperties": { + "^\\.\\S+$": { "enum": ["text", "binary", "file", "empty"] } + } + }, + "define": { + "description": "Defines global identifiers that will be replaced with a specified constant value when found in any JavaScript or TypeScript code including libraries. The value will be used directly. String values must be put in quotes. Identifiers within Angular metadata such as Component Decorators will not be replaced.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "conditions": { + "description": "Custom package resolution conditions used to resolve conditional exports/imports. Defaults to ['module', 'development'/'production']. The following special conditions are always present if the requirements are satisfied: 'default', 'import', 'require', 'browser', 'node'.", + "type": "array", + "items": { + "type": "string" + } + }, + "fileReplacements": { + "description": "Replace compilation source files with other compilation source files in the build.", + "type": "array", + "items": { + "$ref": "#/definitions/fileReplacement" + }, + "default": [] + }, + "outputPath": { + "description": "Specify the output path relative to workspace root.", + "oneOf": [ + { + "type": "object", + "properties": { + "base": { + "type": "string", + "description": "Specify the output path relative to workspace root." + }, + "browser": { + "type": "string", + "pattern": "^[-\\w\\.]*$", + "default": "browser", + "description": "The output directory name of your browser build within the output path base. Defaults to 'browser'." + }, + "server": { + "type": "string", + "pattern": "^[-\\w\\.]*$", + "default": "server", + "description": "The output directory name of your server build within the output path base. Defaults to 'server'." + }, + "media": { + "type": "string", + "pattern": "^[-\\w\\.]+$", + "default": "media", + "description": "The output directory name of your media files within the output browser directory. Defaults to 'media'." + } + }, + "required": ["base"], + "additionalProperties": false + }, + { + "type": "string" + } + ] + }, + "aot": { + "type": "boolean", + "description": "Build using Ahead of Time compilation.", + "x-user-analytics": "ep.ng_aot", + "default": true + }, + "sourceMap": { + "description": "Output source maps for scripts and styles. For more information, see https://angular.dev/reference/configs/workspace-config#source-map-configuration.", + "default": false, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output source maps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output source maps for all styles.", + "default": true + }, + "hidden": { + "type": "boolean", + "description": "Output source maps used for error reporting tools.", + "default": false + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages source maps.", + "default": false + }, + "sourcesContent": { + "type": "boolean", + "description": "Output original source content for files within the source map.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] + }, + "baseHref": { + "type": "string", + "description": "Base url for the application being built." + }, + "verbose": { + "type": "boolean", + "description": "Adds more details to output logging.", + "default": false + }, + "progress": { + "type": "boolean", + "description": "Log progress to the console while building.", + "default": true + }, + "i18nMissingTranslation": { + "type": "string", + "description": "How to handle missing translations for i18n.", + "enum": ["warning", "error", "ignore"], + "default": "warning" + }, + "i18nDuplicateTranslation": { + "type": "string", + "description": "How to handle duplicate translations for i18n.", + "enum": ["warning", "error", "ignore"], + "default": "warning" + }, + "localize": { + "description": "Translate the bundles in one or more locales.", + "oneOf": [ + { + "type": "boolean", + "description": "Translate all locales." + }, + { + "type": "array", + "description": "List of locales ID's to translate.", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-[a-zA-Z]{5,8})?(-x(-[a-zA-Z0-9]{1,8})+)?$" + } + } + ] + }, + "watch": { + "type": "boolean", + "description": "Run build when files change.", + "default": false + }, + "outputHashing": { + "type": "string", + "description": "Define the output filename cache-busting hashing mode.", + "default": "none", + "enum": ["none", "all", "media", "bundles"] + }, + "poll": { + "type": "number", + "description": "Enable and define the file watching poll time period in milliseconds." + }, + "deleteOutputPath": { + "type": "boolean", + "description": "Delete the output path before building.", + "default": true + }, + "preserveSymlinks": { + "type": "boolean", + "description": "Do not use the real path when resolving modules. If unset then will default to `true` if NodeJS option --preserve-symlinks is set." + }, + "extractLicenses": { + "type": "boolean", + "description": "Extract all licenses in a separate file.", + "default": true + }, + "namedChunks": { + "type": "boolean", + "description": "Use file name for lazy loaded chunks.", + "default": false + }, + "subresourceIntegrity": { + "type": "boolean", + "description": "Enables the use of subresource integrity validation.", + "default": false + }, + "serviceWorker": { + "description": "Generates a service worker configuration.", + "default": false, + "oneOf": [ + { + "type": "string", + "description": "Path to ngsw-config.json." + }, + { + "const": false, + "type": "boolean", + "description": "Does not generate a service worker configuration." + } + ] + }, + "index": { + "description": "Configures the generation of the application's HTML index.", + "oneOf": [ + { + "type": "string", + "description": "The path of a file to use for the application's HTML index. The filename of the specified path will be used for the generated file and will be created in the root of the application's configured output path." + }, + { + "type": "object", + "description": "", + "properties": { + "input": { + "type": "string", + "minLength": 1, + "description": "The path of a file to use for the application's generated HTML index." + }, + "output": { + "type": "string", + "minLength": 1, + "default": "index.html", + "description": "The output path of the application's generated HTML index file. The full provided path will be used and will be considered relative to the application's configured output path." + }, + "preloadInitial": { + "type": "boolean", + "default": true, + "description": "Generates 'preload', 'modulepreload', and 'preconnect' link elements for initial application files and resources." + } + }, + "required": ["input"] + }, + { + "const": false, + "type": "boolean", + "description": "Does not generate an `index.html` file." + } + ] + }, + "statsJson": { + "type": "boolean", + "description": "Generates a 'stats.json' file which can be analyzed with https://esbuild.github.io/analyze/.", + "default": false + }, + "budgets": { + "description": "Budget thresholds to ensure parts of your application stay within boundaries which you set.", + "type": "array", + "items": { + "$ref": "#/definitions/budget" + }, + "default": [] + }, + "webWorkerTsConfig": { + "type": "string", + "description": "TypeScript configuration for Web Worker modules." + }, + "crossOrigin": { + "type": "string", + "description": "Define the crossorigin attribute setting of elements that provide CORS support.", + "default": "none", + "enum": ["none", "anonymous", "use-credentials"] + }, + "allowedCommonJsDependencies": { + "description": "A list of CommonJS or AMD packages that are allowed to be used without a build time warning. Use `'*'` to allow all.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "prerender": { + "description": "Prerender (SSG) pages of your application during build time.", + "oneOf": [ + { + "type": "boolean", + "description": "Enable prerending of pages of your application during build time." + }, + { + "type": "object", + "properties": { + "routesFile": { + "type": "string", + "description": "The path to a file that contains a list of all routes to prerender, separated by newlines. This option is useful if you want to prerender routes with parameterized URLs." + }, + "discoverRoutes": { + "type": "boolean", + "description": "Whether the builder should process the Angular Router configuration to find all unparameterized routes and prerender them.", + "default": true + } + }, + "additionalProperties": false + } + ] + }, + "ssr": { + "description": "Server side render (SSR) pages of your application during runtime.", + "default": false, + "oneOf": [ + { + "type": "boolean", + "description": "Enable the server bundles to be written to disk." + }, + { + "type": "object", + "properties": { + "entry": { + "type": "string", + "description": "The server entry-point that when executed will spawn the web server." + }, + "experimentalPlatform": { + "description": "Specifies the platform for which the server bundle is generated. This affects the APIs and modules available in the server-side code. \n\n- `node`: (Default) Generates a bundle optimized for Node.js environments. \n- `neutral`: Generates a platform-neutral bundle suitable for environments like edge workers, and other serverless platforms. This option avoids using Node.js-specific APIs, making the bundle more portable. \n\nPlease note that this feature does not provide polyfills for Node.js modules. Additionally, it is experimental, and the schematics may undergo changes in future versions.", + "default": "node", + "enum": ["node", "neutral"] + } + }, + "additionalProperties": false + } + ] + }, + "appShell": { + "type": "boolean", + "description": "Generates an application shell during build time." + }, + "outputMode": { + "type": "string", + "description": "Defines the build output target. 'static': Generates a static site for deployment on any static hosting service. 'server': Produces an application designed for deployment on a server that supports server-side rendering (SSR).", + "enum": ["static", "server"] + } + }, + "additionalProperties": false, + "required": ["browser", "tsConfig"], + "definitions": { + "assetPattern": { + "oneOf": [ + { + "type": "object", + "properties": { + "followSymlinks": { + "type": "boolean", + "default": false, + "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched." + }, + "glob": { + "type": "string", + "description": "The pattern to match." + }, + "input": { + "type": "string", + "description": "The input directory path in which to apply 'glob'. Defaults to the project root." + }, + "ignore": { + "description": "An array of globs to ignore.", + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "string", + "default": "", + "description": "Absolute path within the output." + } + }, + "additionalProperties": false, + "required": ["glob", "input"] + }, + { + "type": "string" + } + ] + }, + "fileReplacement": { + "type": "object", + "properties": { + "replace": { + "type": "string", + "pattern": "\\.(([cm]?[jt])sx?|json)$" + }, + "with": { + "type": "string", + "pattern": "\\.(([cm]?[jt])sx?|json)$" + } + }, + "additionalProperties": false, + "required": ["replace", "with"] + }, + "budget": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of budget.", + "enum": ["all", "allScript", "any", "anyScript", "anyComponentStyle", "bundle", "initial"] + }, + "name": { + "type": "string", + "description": "The name of the bundle." + }, + "baseline": { + "type": "string", + "description": "The baseline size for comparison." + }, + "maximumWarning": { + "type": "string", + "description": "The maximum threshold for warning relative to the baseline." + }, + "maximumError": { + "type": "string", + "description": "The maximum threshold for error relative to the baseline." + }, + "minimumWarning": { + "type": "string", + "description": "The minimum threshold for warning relative to the baseline." + }, + "minimumError": { + "type": "string", + "description": "The minimum threshold for error relative to the baseline." + }, + "warning": { + "type": "string", + "description": "The threshold for warning relative to the baseline (min & max)." + }, + "error": { + "type": "string", + "description": "The threshold for error relative to the baseline (min & max)." + } + }, + "additionalProperties": false, + "required": ["type"] + } + } +} diff --git a/packages/angular/build/src/builders/application/setup-bundling.ts b/packages/angular/build/src/builders/application/setup-bundling.ts new file mode 100644 index 000000000000..9b47bc67e49d --- /dev/null +++ b/packages/angular/build/src/builders/application/setup-bundling.ts @@ -0,0 +1,197 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 { AngularCompilation } from '../../tools/angular/compilation'; +import { ComponentStylesheetBundler } from '../../tools/esbuild/angular/component-stylesheets'; +import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache'; +import { + createBrowserCodeBundleOptions, + createBrowserPolyfillBundleOptions, + createServerMainCodeBundleOptions, + createServerPolyfillBundleOptions, + createSsrEntryCodeBundleOptions, +} from '../../tools/esbuild/application-code-bundle'; +import { BundlerContext } from '../../tools/esbuild/bundler-context'; +import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts'; +import { createGlobalStylesBundleOptions } from '../../tools/esbuild/global-styles'; +import { getSupportedNodeTargets } from '../../tools/esbuild/utils'; +import { NormalizedApplicationBuildOptions } from './options'; + +/** + * Generates one or more BundlerContext instances based on the builder provided + * configuration. + * @param options The normalized application builder options to use. + * @param browsers An string array of browserslist browsers to support. + * @param codeBundleCache An instance of the TypeScript source file cache. + * @returns An array of BundlerContext objects. + */ +export function setupBundlerContexts( + options: NormalizedApplicationBuildOptions, + target: string[], + codeBundleCache: SourceFileCache, + stylesheetBundler: ComponentStylesheetBundler, + angularCompilation: AngularCompilation, + templateUpdates: Map | undefined, +): { + typescriptContexts: BundlerContext[]; + otherContexts: BundlerContext[]; +} { + const { + outputMode, + serverEntryPoint, + appShellOptions, + prerenderOptions, + ssrOptions, + workspaceRoot, + watch = false, + } = options; + const typescriptContexts = []; + const otherContexts = []; + + // Browser application code + typescriptContexts.push( + new BundlerContext( + workspaceRoot, + watch, + createBrowserCodeBundleOptions( + options, + target, + codeBundleCache, + stylesheetBundler, + angularCompilation, + templateUpdates, + ), + ), + ); + + // Browser polyfills code + const browserPolyfillBundleOptions = createBrowserPolyfillBundleOptions( + options, + target, + codeBundleCache, + stylesheetBundler, + ); + if (browserPolyfillBundleOptions) { + const browserPolyfillContext = new BundlerContext( + workspaceRoot, + watch, + browserPolyfillBundleOptions, + ); + if (typeof browserPolyfillBundleOptions === 'function') { + otherContexts.push(browserPolyfillContext); + } else { + typescriptContexts.push(browserPolyfillContext); + } + } + + // Global Stylesheets + if (options.globalStyles.length > 0) { + for (const initial of [true, false]) { + const bundleOptions = createGlobalStylesBundleOptions(options, target, initial); + if (bundleOptions) { + otherContexts.push(new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial)); + } + } + } + + // Global Scripts + if (options.globalScripts.length > 0) { + for (const initial of [true, false]) { + const bundleOptions = createGlobalScriptsBundleOptions(options, target, initial); + if (bundleOptions) { + otherContexts.push(new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial)); + } + } + } + + // Skip server build when none of the features are enabled. + if (serverEntryPoint && (outputMode || prerenderOptions || appShellOptions || ssrOptions)) { + const nodeTargets = [...target, ...getSupportedNodeTargets()]; + + typescriptContexts.push( + new BundlerContext( + workspaceRoot, + watch, + createServerMainCodeBundleOptions(options, nodeTargets, codeBundleCache, stylesheetBundler), + ), + ); + + if (outputMode && ssrOptions?.entry) { + // New behavior introduced: 'server.ts' is now bundled separately from 'main.server.ts'. + typescriptContexts.push( + new BundlerContext( + workspaceRoot, + watch, + createSsrEntryCodeBundleOptions(options, nodeTargets, codeBundleCache, stylesheetBundler), + ), + ); + } + + // Server polyfills code + const serverPolyfillBundleOptions = createServerPolyfillBundleOptions( + options, + nodeTargets, + codeBundleCache.loadResultCache, + ); + + if (serverPolyfillBundleOptions) { + otherContexts.push(new BundlerContext(workspaceRoot, watch, serverPolyfillBundleOptions)); + } + } + + return { typescriptContexts, otherContexts }; +} + +export function createComponentStyleBundler( + options: NormalizedApplicationBuildOptions, + target: string[], +): ComponentStylesheetBundler { + const { + workspaceRoot, + optimizationOptions, + sourcemapOptions, + outputNames, + externalDependencies, + preserveSymlinks, + stylePreprocessorOptions, + inlineStyleLanguage, + cacheOptions, + tailwindConfiguration, + postcssConfiguration, + publicPath, + } = options; + const incremental = !!options.watch; + + return new ComponentStylesheetBundler( + { + workspaceRoot, + inlineFonts: !!optimizationOptions.fonts.inline, + optimization: !!optimizationOptions.styles.minify, + sourcemap: + // Hidden component stylesheet sourcemaps are inaccessible which is effectively + // the same as being disabled. Disabling has the advantage of avoiding the overhead + // of sourcemap processing. + sourcemapOptions.styles && !sourcemapOptions.hidden ? 'linked' : false, + sourcesContent: sourcemapOptions.sourcesContent, + outputNames, + includePaths: stylePreprocessorOptions?.includePaths, + // string[] | undefined' is not assignable to type '(Version | DeprecationOrId)[] | undefined'. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sass: stylePreprocessorOptions?.sass as any, + externalDependencies, + target, + preserveSymlinks, + tailwindConfiguration, + postcssConfiguration, + cacheOptions, + publicPath, + }, + inlineStyleLanguage, + incremental, + ); +} diff --git a/packages/angular/build/src/builders/application/tests/behavior/angular-aot-metadata_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/angular-aot-metadata_spec.ts new file mode 100644 index 000000000000..8f5f4d18d781 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/angular-aot-metadata_spec.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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: "Angular metadata"', () => { + it('should not emit any AOT class metadata functions', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + harness.expectFile('dist/browser/main.js').content.not.toContain('setClassMetadata'); + }); + + it('should not emit any AOT NgModule scope metadata functions', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + harness.expectFile('dist/browser/main.js').content.not.toContain('setNgModuleScope'); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/browser-support_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/browser-support_spec.ts new file mode 100644 index 000000000000..e281ca8caeb9 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/browser-support_spec.ts @@ -0,0 +1,183 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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: "Browser support"', () => { + it('creates correct sourcemaps when downleveling async functions', async () => { + // Add a JavaScript file with async code + await harness.writeFile( + 'src/async-test.js', + 'async function testJs() { console.log("from-async-js-function"); }', + ); + + // Add an async function to the project as well as JavaScript file + // The type `Void123` is used as a unique identifier for the final sourcemap + // If sourcemaps are not properly propagated then it will not be in the final sourcemap + await harness.modifyFile( + 'src/main.ts', + (content) => + 'import "./async-test";\n' + + content + + '\ntype Void123 = void;' + + `\nasync function testApp(): Promise { console.log("from-async-app-function"); }`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['zone.js'], + sourceMap: { + scripts: true, + }, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.not.toMatch(/\sasync\s+function\s/); + harness.expectFile('dist/browser/main.js.map').content.toContain('Promise'); + }); + + it('downlevels async functions when zone.js is included as a polyfill', async () => { + // Add an async function to the project + await harness.writeFile( + 'src/main.ts', + 'async function test(): Promise { console.log("from-async-function"); }\ntest();', + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['zone.js'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.not.toMatch(/\sasync\s/); + harness.expectFile('dist/browser/main.js').content.toContain('"from-async-function"'); + }); + + it('does not downlevels async functions when zone.js is not included as a polyfill', async () => { + // Add an async function to the project + await harness.writeFile( + 'src/main.ts', + 'async function test(): Promise { console.log("from-async-function"); }\ntest();', + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: [], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.toMatch(/\sasync\s/); + harness.expectFile('dist/browser/main.js').content.toContain('"from-async-function"'); + }); + + it('warns when IE is present in browserslist', async () => { + await harness.writeFile( + '.browserslistrc', + ` + IE 9 + IE 11 + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + expect(logs).toContain( + jasmine.objectContaining({ + level: 'warn', + message: jasmine.stringContaining('ES5 output is not supported'), + }), + ); + + // Don't duplicate the error. + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringContaining("fall outside Angular's browser support"), + }), + ); + }); + + it("warns when targeting a browser outside Angular's minimum support", async () => { + await harness.writeFile('.browserslistrc', 'Chrome >= 100'); + + harness.useTarget('build', BASE_OPTIONS); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + expect(logs).toContain( + jasmine.objectContaining({ + level: 'warn', + message: jasmine.stringContaining("fall outside Angular's browser support"), + }), + ); + }); + + it('downlevels "for await...of" when zone.js is included as a polyfill', async () => { + // Add an async function to the project + await harness.writeFile( + 'src/main.ts', + ` + (async () => { + for await (const o of [1, 2, 3]) { + console.log("for await...of"); + } + })(); + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['zone.js'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.not.toMatch(/\sawait\s/); + harness.expectFile('dist/browser/main.js').content.toContain('"for await...of"'); + }); + + it('does not downlevel "for await...of" when zone.js is not included as a polyfill', async () => { + // Add an async function to the project + await harness.writeFile( + 'src/main.ts', + ` + (async () => { + for await (const o of [1, 2, 3]) { + console.log("for await...of"); + } + })(); + `, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: [], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.toMatch(/\sawait\s/); + harness.expectFile('dist/browser/main.js').content.toContain('"for await...of"'); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/build-conditions_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/build-conditions_spec.ts new file mode 100644 index 000000000000..949588aa9d1e --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/build-conditions_spec.ts @@ -0,0 +1,116 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 { + setupConditionImport, + setTargetMapping, +} from '../../../../../../../../modules/testing/builder/src/dev_prod_mode'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "conditional imports"', () => { + beforeEach(async () => { + await setupConditionImport(harness); + }); + + interface ImportsTestCase { + name: string; + mapping: unknown; + output?: string; + } + + const GOOD_TARGET = './src/good.js'; + const BAD_TARGET = './src/bad.js'; + + const testCases: ImportsTestCase[] = [ + { name: 'simple string', mapping: GOOD_TARGET }, + { + name: 'default fallback without matching condition', + mapping: { + 'never': BAD_TARGET, + 'default': GOOD_TARGET, + }, + }, + { + name: 'development condition', + mapping: { + 'development': BAD_TARGET, + 'default': GOOD_TARGET, + }, + }, + { + name: 'production condition', + mapping: { + 'production': GOOD_TARGET, + 'default': BAD_TARGET, + }, + }, + { + name: 'browser condition (in browser)', + mapping: { + 'browser': GOOD_TARGET, + 'default': BAD_TARGET, + }, + }, + { + name: 'browser condition (in server)', + output: 'server/main.server.mjs', + mapping: { + 'browser': BAD_TARGET, + 'default': GOOD_TARGET, + }, + }, + ]; + + for (const testCase of testCases) { + describe(testCase.name, () => { + beforeEach(async () => { + await setTargetMapping(harness, testCase.mapping); + }); + + it('resolves to expected target', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + ssr: true, + server: 'src/main.ts', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + const outputFile = `dist/${testCase.output ?? 'browser/main.js'}`; + harness.expectFile(outputFile).content.toContain('"good-value"'); + harness.expectFile(outputFile).content.not.toContain('"bad-value"'); + }); + }); + } + + it('fails type-checking when import contains differing type', async () => { + await setTargetMapping(harness, { + 'development': './src/wrong.ts', + 'default': './src/good.ts', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: false, + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('TS2339'), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/component-stylesheets_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/component-stylesheets_spec.ts new file mode 100644 index 000000000000..ecc460bcb405 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/component-stylesheets_spec.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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: "Component Stylesheets"', () => { + it('should successfuly compile with an empty inline style', async () => { + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content.replace('styleUrls', 'styles').replace('./app.component.css', ''); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + + it('should maintain optimized empty Sass stylesheet when original has content', async () => { + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content.replace('./app.component.css', './app.component.scss'); + }); + await harness.removeFile('src/app/app.component.css'); + await harness.writeFile('src/app/app.component.scss', '@import "https://github.com/variables";'); + await harness.writeFile('src/app/_variables.scss', '$value: blue;'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: { + styles: true, + }, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').content.not.toContain('variables'); + }); + + it('should generate an error for a missing stylesheet with AOT', async () => { + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content.replace('./app.component.css', './not-present.css'); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringContaining(`Could not find stylesheet file './not-present.css'`), + }), + ); + }); + + it('should generate an error for a missing stylesheet with JIT', async () => { + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content.replace('./app.component.css', './not-present.css'); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + aot: false, + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringContaining('Could not resolve'), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/component-templates_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/component-templates_spec.ts new file mode 100644 index 000000000000..687ed78dc74c --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/component-templates_spec.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Component Templates"', () => { + it('should generate an error for a missing template', async () => { + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content.replace('./app.component.html', './not-present.html'); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringContaining(`Could not find template file './not-present.html'`), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/builders/application/tests/behavior/csp-nonce_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/csp-nonce_spec.ts new file mode 100644 index 000000000000..eaeff9d1249c --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/csp-nonce_spec.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "CSP Nonce"', () => { + it('should add CSP nonce to scripts when optimization is disabled', async () => { + await harness.modifyFile('src/index.html', (content) => + content.replace(/', + ); + 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 'https://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 'https://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 'https://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 'https://github.com/a';"); + await harness.writeFile('src/other.scss', "@import 'https://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("https://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 "https://github.com/a.css"; + `, + ); + await harness.writeFile( + 'src/a.css', + ` + .a { + background-image: url("https://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 "https://github.com/a"; + `, + ); + await harness.writeFile( + 'src/a.scss', + ` + .a { + background-image: url("https://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("https://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 "https://github.com/a"; + `, + ); + await harness.writeFile( + 'src/a.scss', + ` + .a { + background-image: url("https://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(https://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 'https://github.com/b'; + .a { + background-image: url(https://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(https://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("https://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(https://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("https://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(https://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("https://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 'https://github.com/b'; + .a { + background-image: url(https://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("https://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 'https://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("https://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 'https://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("https://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 'https://github.com/b'; + .a { + background-image: url(https://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("https://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 'https://github.com/b'; + .a { + $asset: my-function-url('https://github.com/logo'); + background-image: url(https://github.com/$asset) + } + `, + 'src/theme/b.scss': `@function my-function-url(https://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("https://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("https://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(https://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(https://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..ca35f7edb46b --- /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. + useAtomics: !process.versions.webcontainer, + 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 6361ada810b6..000000000000 --- a/packages/angular/cli/BUILD +++ /dev/null @@ -1,216 +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("@npm_bazel_typescript//:defs.bzl", "ts_library") -load("//tools:ts_json_schema.bzl", "ts_json_schema") - -package(default_visibility = ["//visibility:public"]) - -ts_library( - name = "angular-cli", - srcs = glob( - ["**/*.ts"], - exclude = [ - "**/*_spec.ts", - "**/*_spec_large.ts", - ], - ), - data = glob([ - "**/*.json", - "**/*.md", - ]), - module_name = "@angular/cli", - # strict_checks = False, - deps = [ - ":command_schemas", - "//packages/angular_devkit/architect", - "//packages/angular_devkit/architect:node", - "//packages/angular_devkit/core", - "//packages/angular_devkit/core:node", - "//packages/angular_devkit/schematics", - "//packages/angular_devkit/schematics:tools", - # @typings: es2017.object - "@npm//@types/debug", - "@npm//debug", - "@npm//@types/node", - "@npm//@types/inquirer", - "@npm//@types/semver", - "@npm//@types/universal-analytics", - "@npm//@types/uuid", - "@npm//ansi-colors", - "@npm//rxjs", - ], -) - -ts_library( - name = "command_schemas", - srcs = [], - deps = [ - ":add_schema", - ":analytics_schema", - ":build_schema", - ":config_schema", - ":deprecated_schema", - ":doc_schema", - ":e2e_schema", - ":easter_egg_schema", - ":generate_schema", - ":help_schema", - ":lint_schema", - ":new_schema", - ":run_schema", - ":serve_schema", - ":test_schema", - ":update_schema", - ":version_schema", - ":xi18n_schema", - ], -) - -ts_json_schema( - name = "analytics_schema", - src = "commands/analytics.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "add_schema", - src = "commands/add.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "build_schema", - src = "commands/build.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "config_schema", - src = "commands/config.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "deprecated_schema", - src = "commands/deprecated.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "doc_schema", - src = "commands/doc.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "e2e_schema", - src = "commands/e2e.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "easter_egg_schema", - src = "commands/easter-egg.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "generate_schema", - src = "commands/generate.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "help_schema", - src = "commands/help.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "lint_schema", - src = "commands/lint.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "new_schema", - src = "commands/new.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "run_schema", - src = "commands/run.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "serve_schema", - src = "commands/serve.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "test_schema", - src = "commands/test.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "update_schema", - src = "commands/update.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "version_schema", - src = "commands/version.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "xi18n_schema", - src = "commands/xi18n.json", - data = [ - "commands/definitions.json", - ], -) 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 fb5082a4d200..4fa87391f04c 100644 --- a/packages/angular/cli/README.md +++ b/packages/angular/cli/README.md @@ -1,269 +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)][license-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) - -### Install Globablly -```bash -npm install -g @angular/cli -``` - -### Install Locally -```bash -npm install @angular/cli -``` - -To run a locally installed version of the angular-cli, you can call `ng` commands directly by adding the `.bin` folder within your local `node_modules` folder to your PATH. The `node_modules` and `.bin` folders are created in the directory where `npm install @angular/cli` was run upon completion of the install command. - -Alternatively, you can install [npx](https://www.npmjs.com/package/npx) and run `npx ng ` within the local directory where `npm install @angular/cli` was run, which will use the locally installed angular-cli. - -### Install Specific Version (Example: 6.1.1) -```bash -npm install -g @angular/cli@6.1.1 -``` - -## 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://angular.io/cli/generate#component) | `ng g component my-new-component` -[Directive](https://angular.io/cli/generate#directive) | `ng g directive my-new-directive` -[Pipe](https://angular.io/cli/generate#pipe) | `ng g pipe my-new-pipe` -[Service](https://angular.io/cli/generate#service) | `ng g service my-new-service` -[Class](https://angular.io/cli/generate#class) | `ng g class my-new-class` -[Guard](https://angular.io/cli/generate#guard) | `ng g guard my-new-guard` -[Interface](https://angular.io/cli/generate#interface) | `ng g interface my-new-interface` -[Enum](https://angular.io/cli/generate#enum) | `ng g enum my-new-enum` -[Module](https://angular.io/cli/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 these 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 -yarn -npm run build -cd dist/@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 built `dist/@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, -meaning that, in order to quickly test any changes you make to the cli project, you should simply just run `npm run build` -again. - -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 E2E test suite, use the `node ./tests/legacy-cli/run_e2e` command. -It can also receive a filename to only run that test (e.g. `node ./tests/legacy-cli/run_e2e tests/legacy-cli/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. - -### Debugging with 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 informations 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. - -To capture a CPU profiling, you can: -1. install the v8-profiler-node8 dependency: `npm install v8-profiler-node8 --no-save` -1. set the NG_CLI_PROFILING Environment variable to the file name you want: - * on Unix systems (Linux & Mac OS X): ̀`export NG_CLI_PROFILING=my-profile` - * on Windows: ̀̀`setx NG_CLI_PROFILING my-profile` - -Then, just run the ng command on which you want to capture a CPU profile. -You will then obtain a `my-profile.cpuprofile` file in the folder from wich you ran the ng command. - -You can use the Chrome Devtools to process it. To do so: -1. open `chrome://inspect/#devices` 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 - -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. - -## Documentation - -The documentation for the Angular CLI is located in this repo's [wiki](https://angular.io/cli). - -## License - -[MIT](https://github.com/angular/angular-cli/blob/master/LICENSE) - - -[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 -[license-url]: https://github.com/angular/angular-cli/blob/master/LICENSE +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 b0be0f9d40d5..000000000000 --- a/packages/angular/cli/bin/ng +++ /dev/null @@ -1,25 +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. -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'; -} - -// Some older versions of Node do not support let or const. -var version = process.version.substr(1).split('.'); -if (Number(version[0]) < 10 || (Number(version[0]) === 10 && Number(version[1]) < 9)) { - process.stderr.write( - 'You are running version ' + process.version + ' of Node.js, which is not supported by Angular CLI 8.0+.\n' + - 'The official Node.js version that is supported is 10.9 or 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.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/bin/postinstall/analytics-prompt.js b/packages/angular/cli/bin/postinstall/analytics-prompt.js deleted file mode 100644 index c1641478d649..000000000000 --- a/packages/angular/cli/bin/postinstall/analytics-prompt.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; -// This file is ES6 because it needs to be executed as is. - -if ('NG_CLI_ANALYTICS' in process.env) { - return; -} - -(async () => { - try { - const analytics = require('../../models/analytics'); - - if (!analytics.hasGlobalAnalyticsConfiguration()) { - await analytics.promptGlobalAnalytics(); - } - } catch (_) {} -})(); diff --git a/packages/angular/cli/bin/postinstall/ng-update-message.js b/packages/angular/cli/bin/postinstall/ng-update-message.js deleted file mode 100755 index 5e4742bdefd5..000000000000 --- a/packages/angular/cli/bin/postinstall/ng-update-message.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; -// This file is ES6 because it needs to be executed as is. - -// 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/postinstall/script.js b/packages/angular/cli/bin/postinstall/script.js deleted file mode 100644 index 676ede7b84c3..000000000000 --- a/packages/angular/cli/bin/postinstall/script.js +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -require('./ng-update-message'); -require('./analytics-prompt'); diff --git a/packages/angular/cli/commands.json b/packages/angular/cli/commands.json deleted file mode 100644 index 8d3849174445..000000000000 --- a/packages/angular/cli/commands.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "add": "./commands/add.json", - "analytics": "./commands/analytics.json", - "build": "./commands/build.json", - "config": "./commands/config.json", - "doc": "./commands/doc.json", - "e2e": "./commands/e2e.json", - "make-this-awesome": "./commands/easter-egg.json", - "generate": "./commands/generate.json", - "get": "./commands/deprecated.json", - "set": "./commands/deprecated.json", - "help": "./commands/help.json", - "lint": "./commands/lint.json", - "new": "./commands/new.json", - "run": "./commands/run.json", - "serve": "./commands/serve.json", - "test": "./commands/test.json", - "update": "./commands/update.json", - "version": "./commands/version.json", - "xi18n": "./commands/xi18n.json" -} diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add-impl.ts deleted file mode 100644 index adcbdc6af316..000000000000 --- a/packages/angular/cli/commands/add-impl.ts +++ /dev/null @@ -1,272 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { analytics, tags } from '@angular-devkit/core'; -import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools'; -import { dirname, join } from 'path'; -import { intersects, prerelease, rcompare, satisfies, valid, validRange } from 'semver'; -import { isPackageNameSafeForAnalytics } from '../models/analytics'; -import { Arguments } from '../models/interface'; -import { SchematicCommand } from '../models/schematic-command'; -import npmInstall from '../tasks/npm-install'; -import { colors } from '../utilities/color'; -import { getPackageManager } from '../utilities/package-manager'; -import { - PackageManifest, - fetchPackageManifest, - fetchPackageMetadata, -} from '../utilities/package-metadata'; -import { Schema as AddCommandSchema } from './add'; - -const npa = require('npm-package-arg'); - -export class AddCommand extends SchematicCommand { - readonly allowPrivateSchematics = true; - readonly allowAdditionalArgs = true; - readonly packageManager = getPackageManager(this.workspace.root); - - async run(options: AddCommandSchema & Arguments) { - if (!options.collection) { - this.logger.fatal( - `The "ng add" command requires a name argument to be specified eg. ` + - `${colors.yellow('ng add [name] ')}. For more details, use "ng help".`, - ); - - return 1; - } - - let packageIdentifier; - try { - packageIdentifier = npa(options.collection); - } catch (e) { - this.logger.error(e.message); - - return 1; - } - - if (packageIdentifier.registry && this.isPackageInstalled(packageIdentifier.name)) { - // Already installed so just run schematic - this.logger.info('Skipping installation: Package already installed'); - - return this.executeSchematic(packageIdentifier.name, options['--']); - } - - const usingYarn = this.packageManager === 'yarn'; - - if (packageIdentifier.type === 'tag' && !packageIdentifier.rawSpec) { - // 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(packageIdentifier.name, this.logger, { - registry: options.registry, - usingYarn, - }); - } catch (e) { - this.logger.error('Unable to fetch package metadata: ' + e.message); - - return 1; - } - - const latestManifest = packageMetadata.tags['latest']; - if (latestManifest && Object.keys(latestManifest.peerDependencies).length === 0) { - if (latestManifest.name === '@angular/pwa') { - const version = await this.findProjectVersion('@angular/cli'); - // tslint:disable-next-line:no-any - const semverOptions = { includePrerelease: true } as any; - - if ( - version && - ((validRange(version) && intersects(version, '7', semverOptions)) || - (valid(version) && satisfies(version, '7', semverOptions))) - ) { - packageIdentifier = npa.resolve('@angular/pwa', '0.12'); - } - } - } else if (!latestManifest || (await this.hasMismatchedPeer(latestManifest))) { - // 'latest' is invalid so search for most recent matching package - const versionManifests = Array.from(packageMetadata.versions.values()).filter( - value => !prerelease(value.version), - ); - - versionManifests.sort((a, b) => rcompare(a.version, b.version, true)); - - let newIdentifier; - for (const versionManifest of versionManifests) { - if (!(await this.hasMismatchedPeer(versionManifest))) { - newIdentifier = npa.resolve(packageIdentifier.name, versionManifest.version); - break; - } - } - - if (!newIdentifier) { - this.logger.warn("Unable to find compatible package. Using 'latest'."); - } else { - packageIdentifier = newIdentifier; - } - } - } - - let collectionName = packageIdentifier.name; - if (!packageIdentifier.registry) { - try { - const manifest = await fetchPackageManifest(packageIdentifier, this.logger, { - registry: options.registry, - usingYarn, - }); - - collectionName = manifest.name; - - if (await this.hasMismatchedPeer(manifest)) { - this.logger.warn( - 'Package has unmet peer dependencies. Adding the package may not succeed.', - ); - } - } catch (e) { - this.logger.error('Unable to fetch package manifest: ' + e.message); - - return 1; - } - } - - await npmInstall(packageIdentifier.raw, this.logger, this.packageManager, this.workspace.root); - - return this.executeSchematic(collectionName, options['--']); - } - - async reportAnalytics( - paths: string[], - options: AddCommandSchema & Arguments, - dimensions: (boolean | number | string)[] = [], - metrics: (boolean | number | string)[] = [], - ): Promise { - const collection = options.collection; - - // Add the collection if it's safe listed. - if (collection && isPackageNameSafeForAnalytics(collection)) { - dimensions[analytics.NgCliAnalyticsDimensions.NgAddCollection] = collection; - } else { - delete dimensions[analytics.NgCliAnalyticsDimensions.NgAddCollection]; - } - - return super.reportAnalytics(paths, options, dimensions, metrics); - } - - private isPackageInstalled(name: string): boolean { - try { - require.resolve(join(name, 'package.json'), { paths: [this.workspace.root] }); - - return true; - } catch (e) { - if (e.code !== 'MODULE_NOT_FOUND') { - throw e; - } - } - - return false; - } - - private async executeSchematic( - collectionName: string, - options: string[] = [], - ): Promise { - const runOptions = { - schematicOptions: options, - workingDir: this.workspace.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; - } - } - - private async findProjectVersion(name: string): Promise { - let installedPackage; - try { - installedPackage = require.resolve(join(name, 'package.json'), { - paths: [this.workspace.root], - }); - } catch {} - - if (installedPackage) { - try { - const installed = await fetchPackageManifest(dirname(installedPackage), this.logger); - - return installed.version; - } catch {} - } - - let projectManifest; - try { - projectManifest = await fetchPackageManifest(this.workspace.root, this.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.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; - } - - // tslint:disable-next-line:no-any - const options = { includePrerelease: true } as any; - - 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/commands/add.json b/packages/angular/cli/commands/add.json deleted file mode 100644 index a44773d6f7c3..000000000000 --- a/packages/angular/cli/commands/add.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/add.json", - "description": "Adds support for an external library to your project.", - "$longDescription": "./add.md", - - "$scope": "in", - "$impl": "./add-impl#AddCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "collection": { - "type": "string", - "description": "The package to be added.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "registry": { - "description": "The NPM registry to use.", - "type": "string", - "oneOf": [ - { - "format": "uri" - }, - { - "format": "hostname" - } - ] - } - }, - "required": [ - ] - }, - { - "$ref": "./definitions.json#/definitions/interactive" - }, - { - "$ref": "./definitions.json#/definitions/base" - } - ] -} diff --git a/packages/angular/cli/commands/add.md b/packages/angular/cli/commands/add.md deleted file mode 100644 index d2bbcd6af692..000000000000 --- a/packages/angular/cli/commands/add.md +++ /dev/null @@ -1,8 +0,0 @@ -Adds the npm package for a published library to your workspace, and configures your default -app project to use that library, in whatever way is specified by the library's schematic. -For example, adding `@angular/pwa` configures your project for PWA support: -```bash -ng add @angular/pwa -``` - -The default app project is the value of `defaultProject` in `angular.json`. diff --git a/packages/angular/cli/commands/analytics-impl.ts b/packages/angular/cli/commands/analytics-impl.ts deleted file mode 100644 index 98209f5207ab..000000000000 --- a/packages/angular/cli/commands/analytics-impl.ts +++ /dev/null @@ -1,96 +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 { - promptGlobalAnalytics, - promptProjectAnalytics, - setAnalyticsConfig, -} from '../models/analytics'; -import { Command } from '../models/command'; -import { Arguments } from '../models/interface'; -import { ProjectSetting, Schema as AnalyticsCommandSchema, SettingOrProject } from './analytics'; - - -export class AnalyticsCommand extends Command { - public async run(options: AnalyticsCommandSchema & Arguments) { - // Our parser does not support positional enums (won't report invalid parameters). Do the - // validation manually. - // TODO(hansl): fix parser to better support positionals. This would be a breaking change. - if (options.settingOrProject === undefined) { - if (options['--']) { - // The user passed positional arguments but they didn't validate. - this.logger.error(`Argument ${JSON.stringify(options['--'][0])} is invalid.`); - this.logger.error(`Please provide one of the following value: on, off, ci or project.`); - - return 1; - } else { - // No argument were passed. - await this.printHelp(options); - - return 2; - } - } else if (options.settingOrProject == SettingOrProject.Project - && options.projectSetting === undefined) { - this.logger.error(`Argument ${JSON.stringify(options.settingOrProject)} requires a second ` - + `argument of one of the following value: on, off.`); - - return 2; - } - - try { - switch (options.settingOrProject) { - case SettingOrProject.Off: - setAnalyticsConfig('global', false); - break; - - case SettingOrProject.On: - setAnalyticsConfig('global', true); - break; - - case SettingOrProject.Ci: - setAnalyticsConfig('global', 'ci'); - break; - - case SettingOrProject.Project: - switch (options.projectSetting) { - case ProjectSetting.Off: - setAnalyticsConfig('local', false); - break; - - case ProjectSetting.On: - setAnalyticsConfig('local', true); - break; - - case ProjectSetting.Prompt: - await promptProjectAnalytics(true); - break; - - default: - await this.printHelp(options); - - return 3; - } - break; - - case SettingOrProject.Prompt: - await promptGlobalAnalytics(true); - break; - - default: - await this.printHelp(options); - - return 4; - } - } catch (err) { - this.logger.fatal(err.message); - - return 1; - } - - return 0; - } -} diff --git a/packages/angular/cli/commands/analytics.json b/packages/angular/cli/commands/analytics.json deleted file mode 100644 index f2e17059161f..000000000000 --- a/packages/angular/cli/commands/analytics.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/analytics.json", - "description": "Configures the gathering of Angular CLI usage metrics. See https://v8.angular.io/cli/usage-analytics-gathering.", - "$longDescription": "", - - "$aliases": [], - "$scope": "all", - "$type": "native", - "$impl": "./analytics-impl#AnalyticsCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "settingOrProject": { - "enum": [ - "on", - "off", - "ci", - "project", - "prompt" - ], - "description": "Directly enables or disables all usage analytics for the user, or prompts the user to set the status interactively, or sets the default status for the project.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "projectSetting": { - "enum": [ - "on", - "off", - "prompt" - ], - "description": "Sets the default analytics enablement status for the project.", - "$default": { - "$source": "argv", - "index": 1 - } - } - }, - "required": [ - "settingOrProject" - ] - }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/build-impl.ts b/packages/angular/cli/commands/build-impl.ts deleted file mode 100644 index 081588c2b9cb..000000000000 --- a/packages/angular/cli/commands/build-impl.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { analytics } from '@angular-devkit/core'; -import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as BuildCommandSchema } from './build'; - -export class BuildCommand extends ArchitectCommand { - public readonly target = 'build'; - - public async run(options: ArchitectCommandOptions & Arguments) { - return this.runArchitectTarget(options); - } - - async reportAnalytics( - paths: string[], - options: BuildCommandSchema & Arguments, - dimensions: (boolean | number | string)[] = [], - metrics: (boolean | number | string)[] = [], - ): Promise { - if (options.buildEventLog !== undefined) { - dimensions[analytics.NgCliAnalyticsDimensions.NgBuildBuildEventLog] = true; - } - - return super.reportAnalytics(paths, options, dimensions, metrics); - } -} diff --git a/packages/angular/cli/commands/build-long.md b/packages/angular/cli/commands/build-long.md deleted file mode 100644 index 4e10498f4cd5..000000000000 --- a/packages/angular/cli/commands/build-long.md +++ /dev/null @@ -1,18 +0,0 @@ -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`, and `watch` options are applied. -All other options apply only to building applications. - -The application builder uses the [webpack](https://webpack.js.org/) build tool, with default configuration options specified in the workspace configuration file (`angular.json`) or with a named alternative configuration. -A "production" configuration is created by default when you use the CLI to create the project, and you can use that configuration by specifying the `--configuration="production"` or the `--prod="true"` option. - -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 either dash-case or camelCase. -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](guide/workspace-config). diff --git a/packages/angular/cli/commands/build.json b/packages/angular/cli/commands/build.json deleted file mode 100644 index 6678ba5d744e..000000000000 --- a/packages/angular/cli/commands/build.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/build.json", - "description": "Compiles an Angular app into an output directory named dist/ at the given output path. Must be executed from within a workspace directory.", - "$longDescription": "./build-long.md", - - "$aliases": [ "b" ], - "$scope": "in", - "$type": "architect", - "$impl": "./build-impl#BuildCommand", - - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$ref": "./definitions.json#/definitions/base" }, - { - "type": "object", - "properties": { - "buildEventLog": { - "type": "string", - "description": "**EXPERIMENTAL** Output file path for Build Event Protocol events" - } - } - } - ] -} diff --git a/packages/angular/cli/commands/config-impl.ts b/packages/angular/cli/commands/config-impl.ts deleted file mode 100644 index f69e5f024dc1..000000000000 --- a/packages/angular/cli/commands/config-impl.ts +++ /dev/null @@ -1,325 +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 { v4 as uuidV4 } from 'uuid'; -import { Command } from '../models/command'; -import { Arguments, CommandScope } from '../models/interface'; -import { - getWorkspace, - getWorkspaceRaw, - migrateLegacyGlobalConfig, - validateWorkspace, -} from '../utilities/config'; -import { Schema as ConfigCommandSchema, Value as ConfigCommandSchemaValue } from './config'; - - -function _validateBoolean(value: string) { - if (('' + value).trim() === 'true') { - return true; - } else if (('' + value).trim() === 'false') { - return false; - } else { - throw new Error(`Invalid value type; expected Boolean, received ${JSON.stringify(value)}.`); - } -} -function _validateNumber(value: string) { - const numberValue = Number(value); - if (!Number.isFinite(numberValue)) { - return numberValue; - } - throw new Error(`Invalid value type; expected Number, received ${JSON.stringify(value)}.`); -} -function _validateString(value: string) { - return value; -} -function _validateAnalytics(value: string) { - if (value === '') { - // Disable analytics. - return null; - } else { - return value; - } -} -function _validateAnalyticsSharingUuid(value: string) { - if (value == '') { - return uuidV4(); - } else { - return value; - } -} -function _validateAnalyticsSharingTracking(value: string) { - if (!value.match(/^GA-\d+-\d+$/)) { - throw new Error(`Invalid GA property ID: ${JSON.stringify(value)}.`); - } - - return value; -} - - -const validCliPaths = new Map JsonValue)>([ - ['cli.warnings.versionMismatch', _validateBoolean], - ['cli.defaultCollection', _validateString], - ['cli.packageManager', _validateString], - ['cli.analytics', _validateAnalytics], - ['cli.analyticsSharing.tracking', _validateAnalyticsSharingTracking], - ['cli.analyticsSharing.uuid', _validateAnalyticsSharingUuid], -]); - -/** - * 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 Error('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 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: ConfigCommandSchemaValue, path: string): JsonValue { - const cliOptionType = validCliPaths.get(path); - if (cliOptionType) { - return cliOptionType('' + value); - } - - 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 async run(options: ConfigCommandSchema & Arguments) { - const level = options.global ? 'global' : 'local'; - - if (!options.global) { - await this.validateScope(CommandScope.InProject); - } - - 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: ConfigCommandSchema) { - let value; - if (options.jsonPath) { - if (options.jsonPath === 'cli.warnings.typescriptMismatch') { - // NOTE: Remove this in 9.0. - this.logger.warn('The "typescriptMismatch" warning has been removed in 8.0.'); - // Since there is no actual warning, this value is always false. - this.logger.info('false'); - - return 0; - } - - 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()); - } - - return 0; - } - - private set(options: ConfigCommandSchema) { - if (!options.jsonPath || !options.jsonPath.trim()) { - throw new Error('Invalid Path.'); - } - - if (options.jsonPath === 'cli.warnings.typescriptMismatch') { - // NOTE: Remove this in 9.0. - this.logger.warn('The "typescriptMismatch" warning has been removed in 8.0.'); - - return 0; - } - - 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); - - return 0; - } - -} diff --git a/packages/angular/cli/commands/config-long.md b/packages/angular/cli/commands/config-long.md deleted file mode 100644 index 5f6052b3894a..000000000000 --- a/packages/angular/cli/commands/config-long.md +++ /dev/null @@ -1,13 +0,0 @@ -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 in either camelCase or dash-case. - -For further details, see [Workspace Configuration](guide/workspace-config). - -For configuration of CLI usage analytics, see [Gathering an Viewing CLI Usage Analytics](./usage-analytics-gathering). \ No newline at end of file diff --git a/packages/angular/cli/commands/config.json b/packages/angular/cli/commands/config.json deleted file mode 100644 index d0c3d59520b3..000000000000 --- a/packages/angular/cli/commands/config.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/config.json", - "description": "Retrieves or sets Angular configuration values in the angular.json file for the workspace.", - "$longDescription": "", - - "$aliases": [], - "$scope": "all", - "$type": "native", - "$impl": "./config-impl#ConfigCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "jsonPath": { - "type": "string", - "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.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "value": { - "type": ["string", "number", "boolean"], - "description": "If provided, a new value for the given configuration key.", - "$default": { - "$source": "argv", - "index": 1 - } - }, - "global": { - "type": "boolean", - "description": "When true, accesses the global configuration in the caller's home directory.", - "default": false, - "aliases": ["g"] - } - }, - "required": [ - ] - }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/definitions.json b/packages/angular/cli/commands/definitions.json deleted file mode 100644 index 1713520fc1d1..000000000000 --- a/packages/angular/cli/commands/definitions.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/definitions.json", - - "definitions": { - "architect": { - "properties": { - "project": { - "type": "string", - "description": "The name of the project to build. Can be an application or a library.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "A named build target, as specified in the \"configurations\" section of angular.json.\nEach named target is accompanied by a configuration of option defaults for that target.\nSetting this explicitly overrides the \"--prod\" flag", - "type": "string", - "aliases": [ - "c" - ] - }, - "prod": { - "description": "Shorthand for \"--configuration=production\".\nWhen true, sets the build configuration to the production target.\nBy default, the production target is set up in the workspace configuration such that all builds make use of bundling, limited tree-shaking, and also limited dead code elimination.", - "type": "boolean" - } - } - }, - "base": { - "type": "object", - "properties": { - "help": { - "enum": [true, false, "json", "JSON"], - "description": "Shows a help message for this command in the console.", - "default": false - } - } - }, - "schematic": { - "properties": { - "dryRun": { - "type": "boolean", - "default": false, - "aliases": [ "d" ], - "description": "When true, runs through and reports activity without writing out results." - }, - "force": { - "type": "boolean", - "default": false, - "aliases": [ "f" ], - "description": "When true, forces overwriting of existing files." - } - } - }, - "interactive": { - "properties": { - "interactive": { - "type": "boolean", - "default": "true", - "description": "When false, disables interactive input prompts." - }, - "defaults": { - "type": "boolean", - "default": "false", - "description": "When true, disables interactive input prompts for options with a default." - } - } - } - } -} diff --git a/packages/angular/cli/commands/deprecated-impl.ts b/packages/angular/cli/commands/deprecated-impl.ts deleted file mode 100644 index 7887eac8d04d..000000000000 --- a/packages/angular/cli/commands/deprecated-impl.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Command } from '../models/command'; -import { Schema as DeprecatedCommandSchema } from './deprecated'; - -export class DeprecatedCommand extends Command { - public async run() { - let message = 'The "${this.description.name}" command has been deprecated.'; - if (this.description.name == 'get' || this.description.name == 'set') { - message = 'get/set have been deprecated in favor of the config command.'; - } - - this.logger.error(message); - - return 0; - } -} diff --git a/packages/angular/cli/commands/deprecated.json b/packages/angular/cli/commands/deprecated.json deleted file mode 100644 index 05dd86855d95..000000000000 --- a/packages/angular/cli/commands/deprecated.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/deprecated.json", - "description": "Deprecated in favor of config command.", - "$longDescription": "", - - "$impl": "./deprecated-impl#DeprecatedCommand", - "$hidden": true, - "$type": "deprecated", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/doc-impl.ts b/packages/angular/cli/commands/doc-impl.ts deleted file mode 100644 index 2330f9c10005..000000000000 --- a/packages/angular/cli/commands/doc-impl.ts +++ /dev/null @@ -1,60 +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'; -import { Arguments } from '../models/interface'; -import { Schema as DocCommandSchema } from './doc'; - -const open = require('open'); - -export class DocCommand extends Command { - public async run(options: DocCommandSchema & Arguments) { - if (!options.keyword) { - this.logger.error('You should specify a keyword, for instance, `ng doc ActivatedRoute`.'); - - return 0; - } - - let domain = 'angular.io'; - - if (options.version) { - // version can either be a string containing "next" - if (options.version == 'next') { - domain = 'next.angular.io'; - // or a number where version must be a valid Angular version (i.e. not 0, 1 or 3) - } else if (!isNaN(+options.version) && ![0, 1, 3].includes(+options.version)) { - domain = `v${options.version}.angular.io`; - } else { - this.logger.error('Version should either be a number (2, 4, 5, 6...) or "next"'); - - return 0; - } - } else { - // we try to get the current Angular version of the project - // and use it if we can find it - try { - /* tslint:disable-next-line:no-implicit-dependencies */ - const currentNgVersion = require('@angular/core').VERSION.major; - domain = `v${currentNgVersion}.angular.io`; - } catch (e) {} - } - - let searchUrl = `https://${domain}/api?query=${options.keyword}`; - - if (options.search) { - searchUrl = `https://www.google.com/search?q=site%3A${domain}+${options.keyword}`; - } - - // We should wrap `open` in a new Promise because `open` is already resolved - await new Promise(() => { - open(searchUrl, { - wait: false, - }); - }); - } -} diff --git a/packages/angular/cli/commands/doc.json b/packages/angular/cli/commands/doc.json deleted file mode 100644 index b43f448d03cb..000000000000 --- a/packages/angular/cli/commands/doc.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/doc.json", - "description": "Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword.", - "$longDescription": "", - - "$aliases": [ "d" ], - "$type": "native", - "$impl": "./doc-impl#DocCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "keyword": { - "type": "string", - "description": "The keyword to search for, as provided in the search bar in angular.io.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "search": { - "aliases": ["s"], - "type": "boolean", - "default": false, - "description": "When true, searches all of angular.io. Otherwise, searches only API reference documentation." - }, - "version" : { - "oneOf": [ - { - "type": "number", - "minimum": 4 - }, - { - "enum": [2, "next"] - } - ], - "description": "Contains the version of Angular to use for the documentation. If not provided, the command uses your current Angular core version." - } - }, - "required": [ - ] - }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/e2e-impl.ts b/packages/angular/cli/commands/e2e-impl.ts deleted file mode 100644 index f24a44ca4122..000000000000 --- a/packages/angular/cli/commands/e2e-impl.ts +++ /dev/null @@ -1,21 +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 } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as E2eCommandSchema } from './e2e'; - - -export class E2eCommand extends ArchitectCommand { - public readonly target = 'e2e'; - public readonly multiTarget = true; - - public async run(options: E2eCommandSchema & Arguments) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/e2e-long.md b/packages/angular/cli/commands/e2e-long.md deleted file mode 100644 index 6b651df713d9..000000000000 --- a/packages/angular/cli/commands/e2e-long.md +++ /dev/null @@ -1,2 +0,0 @@ -Must be executed from within a workspace directory. -When a project name is not supplied, it will execute for all projects. \ No newline at end of file diff --git a/packages/angular/cli/commands/e2e.json b/packages/angular/cli/commands/e2e.json deleted file mode 100644 index 92c626d4c63e..000000000000 --- a/packages/angular/cli/commands/e2e.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/e2e.json", - "description": "Builds and serves an Angular app, then runs end-to-end tests using Protractor.", - "$longDescription": "./e2e-long.md", - - "$aliases": [ "e" ], - "$scope": "in", - "$type": "architect", - "$impl": "./e2e-impl#E2eCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/easter-egg-impl.ts b/packages/angular/cli/commands/easter-egg-impl.ts deleted file mode 100644 index 8e70d46dc827..000000000000 --- a/packages/angular/cli/commands/easter-egg-impl.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 { Command } from '../models/command'; -import { colors } from '../utilities/color'; -import { Schema as AwesomeCommandSchema } from './easter-egg'; - -function pickOne(of: string[]): string { - return of[Math.floor(Math.random() * of.length)]; -} - -export class AwesomeCommand extends Command { - async 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(colors.green(phrase)); - } -} diff --git a/packages/angular/cli/commands/easter-egg.json b/packages/angular/cli/commands/easter-egg.json deleted file mode 100644 index d0a7e94189e9..000000000000 --- a/packages/angular/cli/commands/easter-egg.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/easter-egg.json", - "description": "", - "$longDescription": "", - "$hidden": true, - - "$impl": "./easter-egg-impl#AwesomeCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts deleted file mode 100644 index f384d30e87bc..000000000000 --- a/packages/angular/cli/commands/generate-impl.ts +++ /dev/null @@ -1,121 +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 { Arguments, SubCommandDescription } from '../models/interface'; -import { SchematicCommand } from '../models/schematic-command'; -import { colors } from '../utilities/color'; -import { parseJsonSchemaToSubCommandDescription } from '../utilities/json-schema'; -import { Schema as GenerateCommandSchema } from './generate'; - -export class GenerateCommand extends SchematicCommand { - // Allows us to resolve aliases before reporting analytics - longSchematicName: string | undefined; - - async initialize(options: GenerateCommandSchema & Arguments) { - // Fill up the schematics property of the command description. - const [collectionName, schematicName] = this.parseSchematicInfo(options); - this.collectionName = collectionName; - this.schematicName = schematicName; - - await super.initialize(options); - - const collection = this.getCollection(collectionName); - const subcommands: { [name: string]: SubCommandDescription } = {}; - - const schematicNames = schematicName ? [schematicName] : collection.listSchematicNames(); - // Sort as a courtesy for the user. - schematicNames.sort(); - - for (const name of schematicNames) { - const schematic = this.getSchematic(collection, name, true); - this.longSchematicName = schematic.description.name; - let subcommand: SubCommandDescription; - if (schematic.description.schemaJson) { - subcommand = await parseJsonSchemaToSubCommandDescription( - name, - schematic.description.path, - this._workflow.registry, - schematic.description.schemaJson, - ); - } else { - continue; - } - - if (this.getDefaultSchematicCollection() == collectionName) { - subcommands[name] = subcommand; - } else { - subcommands[`${collectionName}:${name}`] = subcommand; - } - } - - this.description.options.forEach(option => { - if (option.name == 'schematic') { - option.subcommands = subcommands; - } - }); - } - - public async run(options: GenerateCommandSchema & Arguments) { - if (!this.schematicName || !this.collectionName) { - return this.printHelp(options); - } - - return this.runSchematic({ - collectionName: this.collectionName, - schematicName: this.schematicName, - schematicOptions: options['--'] || [], - debug: !!options.debug || false, - dryRun: !!options.dryRun || false, - force: !!options.force || false, - }); - } - - async reportAnalytics( - paths: string[], - options: GenerateCommandSchema & Arguments, - ): Promise { - const [collectionName, schematicName] = this.parseSchematicInfo(options); - - if (!schematicName || !collectionName) { - return; - } - const escapedSchematicName = (this.longSchematicName || schematicName).replace(/\//g, '_'); - - return super.reportAnalytics( - ['generate', collectionName.replace(/\//g, '_'), escapedSchematicName], - options, - ); - } - - private parseSchematicInfo(options: { schematic?: string }): [string, string | undefined] { - let collectionName = this.getDefaultSchematicCollection(); - - let schematicName = options.schematic; - - if (schematicName) { - if (schematicName.includes(':')) { - [collectionName, schematicName] = schematicName.split(':', 2); - } - } - - return [collectionName, schematicName]; - } - - public async printHelp(options: GenerateCommandSchema & Arguments) { - await super.printHelp(options); - - this.logger.info(''); - // Find the generate subcommand. - const subcommand = this.description.options.filter(x => x.subcommands)[0]; - if (Object.keys((subcommand && subcommand.subcommands) || {}).length == 1) { - this.logger.info(`\nTo see help for a schematic run:`); - this.logger.info(colors.cyan(` ng generate --help`)); - } - - return 0; - } -} diff --git a/packages/angular/cli/commands/generate.json b/packages/angular/cli/commands/generate.json deleted file mode 100644 index be287dd307c8..000000000000 --- a/packages/angular/cli/commands/generate.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/generate.json", - "description": "Generates and/or modifies files based on a schematic.", - "$longDescription": "", - - "$aliases": [ "g" ], - "$scope": "in", - "$type": "schematics", - "$impl": "./generate-impl#GenerateCommand", - - "allOf": [ - { - "type": "object", - "properties": { - "schematic": { - "type": "string", - "description": "The schematic or collection:schematic to generate.", - "$default": { - "$source": "argv", - "index": 0 - } - } - }, - "required": [ - ] - }, - { "$ref": "./definitions.json#/definitions/base" }, - { "$ref": "./definitions.json#/definitions/schematic" }, - { "$ref": "./definitions.json#/definitions/interactive" } - ] -} diff --git a/packages/angular/cli/commands/help-impl.ts b/packages/angular/cli/commands/help-impl.ts deleted file mode 100644 index 9c30c5e2f608..000000000000 --- a/packages/angular/cli/commands/help-impl.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 } from '../models/command'; -import { colors } from '../utilities/color'; -import { Schema as HelpCommandSchema } from './help'; - -export class HelpCommand extends Command { - async run() { - this.logger.info(`Available Commands:`); - - for (const cmd of Object.values(await Command.commandMap())) { - if (cmd.hidden) { - continue; - } - - const aliasInfo = cmd.aliases.length > 0 ? ` (${cmd.aliases.join(', ')})` : ''; - this.logger.info(` ${colors.cyan(cmd.name)}${aliasInfo} ${cmd.description}`); - } - this.logger.info(`\nFor more detailed help run "ng [command name] --help"`); - } -} diff --git a/packages/angular/cli/commands/help-long.md b/packages/angular/cli/commands/help-long.md deleted file mode 100644 index b104a1a6c03e..000000000000 --- a/packages/angular/cli/commands/help-long.md +++ /dev/null @@ -1,7 +0,0 @@ - For help with individual commands, use the `--help` or `-h` option with the command. - - For example, - - ```sh - ng help serve - ``` diff --git a/packages/angular/cli/commands/help.json b/packages/angular/cli/commands/help.json deleted file mode 100644 index b8df614b75b2..000000000000 --- a/packages/angular/cli/commands/help.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/help.json", - "description": "Lists available commands and their short descriptions.", - "$longDescription": "./help-long.md", - - "$scope": "all", - "$aliases": [], - "$impl": "./help-impl#HelpCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/lint-impl.ts b/packages/angular/cli/commands/lint-impl.ts deleted file mode 100644 index 6edd8556f994..000000000000 --- a/packages/angular/cli/commands/lint-impl.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 - */ -import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as LintCommandSchema } from './lint'; - -export class LintCommand extends ArchitectCommand { - public readonly target = 'lint'; - public readonly multiTarget = true; - - public async run(options: ArchitectCommandOptions & Arguments) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/lint-long.md b/packages/angular/cli/commands/lint-long.md deleted file mode 100644 index 03917ffb252e..000000000000 --- a/packages/angular/cli/commands/lint-long.md +++ /dev/null @@ -1,4 +0,0 @@ -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. - -The default linting tool is [TSLint](https://palantir.github.io/tslint/), and the default configuration is specified in the project's `tslint.json` file. \ No newline at end of file diff --git a/packages/angular/cli/commands/lint.json b/packages/angular/cli/commands/lint.json deleted file mode 100644 index eed1cda035c6..000000000000 --- a/packages/angular/cli/commands/lint.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/lint.json", - "description": "Runs linting tools on Angular app code in a given project folder.", - "$longDescription": "./lint-long.md", - - "$aliases": [ "l" ], - "$scope": "in", - "$type": "architect", - "$impl": "./lint-impl#LintCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "project": { - "type": "string", - "description": "The name of the project to lint.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "The linting configuration to use.", - "type": "string", - "aliases": [ - "c" - ] - } - }, - "required": [ - ] - }, - { - "$ref": "./definitions.json#/definitions/base" - } - ] -} diff --git a/packages/angular/cli/commands/new-impl.ts b/packages/angular/cli/commands/new-impl.ts deleted file mode 100644 index 1b1761680088..000000000000 --- a/packages/angular/cli/commands/new-impl.ts +++ /dev/null @@ -1,49 +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 { Arguments } from '../models/interface'; -import { SchematicCommand } from '../models/schematic-command'; -import { Schema as NewCommandSchema } from './new'; - - -export class NewCommand extends SchematicCommand { - public readonly allowMissingWorkspace = true; - schematicName = 'ng-new'; - - async initialize(options: NewCommandSchema & Arguments) { - if (options.collection) { - this.collectionName = options.collection; - } else { - this.collectionName = this.parseCollectionName(options); - } - - return super.initialize(options); - } - - public async run(options: NewCommandSchema & Arguments) { - // Register the version of the CLI in the registry. - const packageJson = require('../package.json'); - const version = packageJson.version; - - this._workflow.registry.addSmartDefaultProvider('ng-cli-version', () => version); - - return this.runSchematic({ - collectionName: this.collectionName, - schematicName: this.schematicName, - schematicOptions: options['--'] || [], - debug: !!options.debug, - dryRun: !!options.dryRun, - force: !!options.force, - }); - } - - private parseCollectionName(options: any): string { - return options.collection || this.getDefaultSchematicCollection(); - } -} diff --git a/packages/angular/cli/commands/new.json b/packages/angular/cli/commands/new.json deleted file mode 100644 index 89cfbda500dd..000000000000 --- a/packages/angular/cli/commands/new.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/new.json", - "description": "Creates a new workspace and an initial Angular app.", - "$longDescription": "./new.md", - - "$aliases": [ "n" ], - "$scope": "out", - "$type": "schematic", - "$impl": "./new-impl#NewCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "collection": { - "type": "string", - "aliases": [ "c" ], - "description": "A collection of schematics to use in generating the initial app." - }, - "verbose": { - "type": "boolean", - "default": false, - "aliases": [ "v" ], - "description": "When true, adds more details to output logging." - } - }, - "required": [] - }, - { "$ref": "./definitions.json#/definitions/base" }, - { "$ref": "./definitions.json#/definitions/schematic" }, - { "$ref": "./definitions.json#/definitions/interactive" } - ] -} diff --git a/packages/angular/cli/commands/new.md b/packages/angular/cli/commands/new.md deleted file mode 100644 index 822deb745ee1..000000000000 --- a/packages/angular/cli/commands/new.md +++ /dev/null @@ -1,16 +0,0 @@ -Creates and initializes a new Angular app 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 app (with the same name as the workspace) are placed in the `src/` subfolder. Corresponding end-to-end tests are placed in the `e2e/` subfolder. - -* The new app's configuration appears in the `projects` section of the `angular.json` workspace configuration file, under its project name. - -* Subsequent apps that you generate in the workspace reside in the `projects/` subfolder. - -If you plan to have multiple apps in the workspace, you can create an empty workspace by setting the `--createApplication` option to false. -You can then use `ng generate application` to create an initial app. -This allows a workspace name different from the initial app name, and ensures that all apps reside in the `/projects` subfolder, matching the structure of the configuration file. \ No newline at end of file diff --git a/packages/angular/cli/commands/run-impl.ts b/packages/angular/cli/commands/run-impl.ts deleted file mode 100644 index feefe731fa5b..000000000000 --- a/packages/angular/cli/commands/run-impl.ts +++ /dev/null @@ -1,21 +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 { Arguments } from '../models/interface'; -import { Schema as RunCommandSchema } from './run'; - -export class RunCommand extends ArchitectCommand { - public async run(options: ArchitectCommandOptions & Arguments) { - if (options.target) { - return this.runArchitectTarget(options); - } else { - throw new Error('Invalid architect target.'); - } - } -} diff --git a/packages/angular/cli/commands/run-long.md b/packages/angular/cli/commands/run-long.md deleted file mode 100644 index a95bbd78a27a..000000000000 --- a/packages/angular/cli/commands/run-long.md +++ /dev/null @@ -1,16 +0,0 @@ -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. -If you do so, you can run them from the command line using the `ng run` command. -Execute the command using the following format. - -``` -ng run project:target[:configuration] -``` \ No newline at end of file diff --git a/packages/angular/cli/commands/run.json b/packages/angular/cli/commands/run.json deleted file mode 100644 index 4111cc014a67..000000000000 --- a/packages/angular/cli/commands/run.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/run.json", - "description": "Runs an Architect target with an optional custom builder configuration defined in your project.", - "$longDescription": "./run-long.md", - - "$aliases": [], - "$scope": "in", - "$type": "architect", - "$impl": "./run-impl#RunCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "target": { - "type": "string", - "description": "The Architect target to run.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "A named builder configuration, defined in the \"configurations\" section of angular.json.\nThe builder uses the named configuration to run the given target.", - "type": "string", - "aliases": [ "c" ] - } - }, - "required": [ - ] - }, - { - "$ref": "./definitions.json#/definitions/base" - } - ] -} diff --git a/packages/angular/cli/commands/serve-impl.ts b/packages/angular/cli/commands/serve-impl.ts deleted file mode 100644 index dff1855e6e7a..000000000000 --- a/packages/angular/cli/commands/serve-impl.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { analytics } from '@angular-devkit/core'; -import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as BuildCommandSchema } from './build'; -import { Schema as ServeCommandSchema } from './serve'; - -export class ServeCommand extends ArchitectCommand { - public readonly target = 'serve'; - - public validate(_options: ArchitectCommandOptions & Arguments) { - return true; - } - - public async run(options: ArchitectCommandOptions & Arguments) { - return this.runArchitectTarget(options); - } - - async reportAnalytics( - paths: string[], - options: BuildCommandSchema & Arguments, - dimensions: (boolean | number | string)[] = [], - metrics: (boolean | number | string)[] = [], - ): Promise { - if (options.buildEventLog !== undefined) { - dimensions[analytics.NgCliAnalyticsDimensions.NgBuildBuildEventLog] = true; - } - - return super.reportAnalytics(paths, options, dimensions, metrics); - } -} diff --git a/packages/angular/cli/commands/serve.json b/packages/angular/cli/commands/serve.json deleted file mode 100644 index e19f0e5bab56..000000000000 --- a/packages/angular/cli/commands/serve.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/serve.json", - "description": "Builds and serves your app, rebuilding on file changes.", - "$longDescription": "", - - "$aliases": [ "s" ], - "$scope": "in", - "$type": "architect", - "$impl": "./serve-impl#ServeCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$ref": "./definitions.json#/definitions/base" }, - { - "type": "object", - "properties": { - "buildEventLog": { - "type": "string", - "description": "**EXPERIMENTAL** Output file path for Build Event Protocol events" - } - } - } - ] -} diff --git a/packages/angular/cli/commands/test-impl.ts b/packages/angular/cli/commands/test-impl.ts deleted file mode 100644 index 28f09df4d7b2..000000000000 --- a/packages/angular/cli/commands/test-impl.ts +++ /dev/null @@ -1,20 +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 { Arguments } from '../models/interface'; -import { Schema as TestCommandSchema } from './test'; - -export class TestCommand extends ArchitectCommand { - public readonly target = 'test'; - public readonly multiTarget = true; - - public async run(options: ArchitectCommandOptions & Arguments) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/test-long.md b/packages/angular/cli/commands/test-long.md deleted file mode 100644 index 64dae312ab47..000000000000 --- a/packages/angular/cli/commands/test-long.md +++ /dev/null @@ -1,2 +0,0 @@ -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. \ No newline at end of file diff --git a/packages/angular/cli/commands/test.json b/packages/angular/cli/commands/test.json deleted file mode 100644 index 3b330a9cb71e..000000000000 --- a/packages/angular/cli/commands/test.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/test.json", - "description": "Runs unit tests in a project.", - "$longDescription": "./test-long.md", - - "$aliases": [ "t" ], - "$scope": "in", - "$type": "architect", - "$impl": "./test-impl#TestCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/update-impl.ts b/packages/angular/cli/commands/update-impl.ts deleted file mode 100644 index 64d63baf812c..000000000000 --- a/packages/angular/cli/commands/update-impl.ts +++ /dev/null @@ -1,372 +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 { execSync } from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as semver from 'semver'; -import { Arguments, Option } from '../models/interface'; -import { SchematicCommand } from '../models/schematic-command'; -import { findUp } from '../utilities/find-up'; -import { getPackageManager } from '../utilities/package-manager'; -import { - PackageIdentifier, - PackageManifest, - fetchPackageMetadata, -} from '../utilities/package-metadata'; -import { - PackageTreeActual, - findNodeDependencies, - readPackageTree, -} from '../utilities/package-tree'; -import { Schema as UpdateCommandSchema } from './update'; - -const npa = require('npm-package-arg'); - -const oldConfigFileNames = ['.angular-cli.json', 'angular-cli.json']; - -export class UpdateCommand extends SchematicCommand { - public readonly allowMissingWorkspace = true; - - async parseArguments(_schematicOptions: string[], _schema: Option[]): Promise { - return {}; - } - - // tslint:disable-next-line:no-big-function - async run(options: UpdateCommandSchema & Arguments) { - const packages: PackageIdentifier[] = []; - for (const request of options['--'] || []) { - try { - const packageIdentifier: PackageIdentifier = npa(request); - - // only registry identifiers are supported - if (!packageIdentifier.registry) { - this.logger.error(`Package '${request}' is not a registry package identifer.`); - - return 1; - } - - if (packages.some(v => v.name === packageIdentifier.name)) { - this.logger.error(`Duplicate package '${packageIdentifier.name}' specified.`); - - return 1; - } - - // If next option is used and no specifier supplied, use next tag - if (options.next && !packageIdentifier.rawSpec) { - packageIdentifier.fetchSpec = 'next'; - } - - packages.push(packageIdentifier); - } catch (e) { - this.logger.error(e.message); - - return 1; - } - } - - if (options.all && packages.length > 0) { - this.logger.error('Cannot specify packages when using the "all" option.'); - - return 1; - } else if (options.all && options.migrateOnly) { - this.logger.error('Cannot use "all" option with "migrate-only" option.'); - - return 1; - } else if (!options.migrateOnly && (options.from || options.to)) { - this.logger.error('Can only use "from" or "to" options with "migrate-only" option.'); - - return 1; - } - - // If not asking for status then check for a clean git repository. - // This allows the user to easily reset any changes from the update. - const statusCheck = packages.length === 0 && !options.all; - if (!statusCheck && !this.checkCleanGit()) { - if (options.allowDirty) { - this.logger.warn( - 'Repository is not clean. Update changes will be mixed with pre-existing changes.', - ); - } else { - this.logger.error( - 'Repository is not clean. Please commit or stash any changes before updating.', - ); - - return 2; - } - } - - const packageManager = getPackageManager(this.workspace.root); - this.logger.info(`Using package manager: '${packageManager}'`); - - // Special handling for Angular CLI 1.x migrations - if ( - options.migrateOnly === undefined && - options.from === undefined && - !options.all && - packages.length === 1 && - packages[0].name === '@angular/cli' && - this.workspace.configFile && - oldConfigFileNames.includes(this.workspace.configFile) - ) { - options.migrateOnly = true; - options.from = '1.0.0'; - } - - this.logger.info('Collecting installed dependencies...'); - - const packageTree = await readPackageTree(this.workspace.root); - const rootDependencies = findNodeDependencies(packageTree); - - this.logger.info(`Found ${Object.keys(rootDependencies).length} dependencies.`); - - if (options.all || packages.length === 0) { - // Either update all packages or show status - return this.runSchematic({ - collectionName: '@schematics/update', - schematicName: 'update', - dryRun: !!options.dryRun, - showNothingDone: false, - additionalOptions: { - force: options.force || false, - next: options.next || false, - packageManager, - packages: options.all ? Object.keys(rootDependencies) : [], - }, - }); - } - - if (options.migrateOnly) { - if (!options.from) { - this.logger.error('"from" option is required when using the "migrate-only" option.'); - - return 1; - } else if (packages.length !== 1) { - this.logger.error( - 'A single package must be specified when using the "migrate-only" option.', - ); - - return 1; - } - - if (options.next) { - this.logger.warn('"next" option has no effect when using "migrate-only" option.'); - } - - const packageName = packages[0].name; - let packageNode = rootDependencies[packageName]; - if (typeof packageNode === 'string') { - this.logger.error('Package found in package.json but is not installed.'); - - return 1; - } else if (!packageNode) { - // 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 child = packageTree.children.find(c => c.name === packageName); - if (child) { - // A link represents a symlinked package so use the actual in this case - packageNode = child.isLink ? child.target : child; - } - - if (!packageNode) { - this.logger.error('Package is not installed.'); - - return 1; - } - } - - const updateMetadata = packageNode.package['ng-update']; - let migrations = updateMetadata && updateMetadata.migrations; - if (migrations === undefined) { - this.logger.error('Package does not provide migrations.'); - - return 1; - } else if (typeof migrations !== 'string') { - this.logger.error('Package contains a malformed migrations field.'); - - return 1; - } else if (path.posix.isAbsolute(migrations) || path.win32.isAbsolute(migrations)) { - this.logger.error( - 'Package contains an invalid migrations field. Absolute paths are not permitted.', - ); - - return 1; - } - - // Normalize slashes - migrations = migrations.replace(/\\/g, '/'); - - if (migrations.startsWith('../')) { - this.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(packageNode.path, migrations); - if (fs.existsSync(localMigrations)) { - migrations = localMigrations; - } else { - // Try to resolve from package location. - // This avoids issues with package hoisting. - try { - migrations = require.resolve(migrations, { paths: [packageNode.path] }); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - this.logger.error('Migrations for package were not found.'); - } else { - this.logger.error(`Unable to resolve migrations for package. [${e.message}]`); - } - - return 1; - } - } - - return this.runSchematic({ - collectionName: '@schematics/update', - schematicName: 'migrate', - dryRun: !!options.dryRun, - force: false, - showNothingDone: false, - additionalOptions: { - package: packageName, - collection: migrations, - from: options.from, - to: options.to || packageNode.package.version, - }, - }); - } - - const requests: { - identifier: PackageIdentifier; - node: PackageTreeActual | string; - }[] = []; - - // Validate packages actually are part of the workspace - for (const pkg of packages) { - const node = rootDependencies[pkg.name]; - if (!node) { - this.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' && - typeof node === 'object' && - node.package.version === pkg.fetchSpec - ) { - this.logger.info(`Package '${pkg.name}' is already at '${pkg.fetchSpec}'.`); - continue; - } - - requests.push({ identifier: pkg, node }); - } - - if (requests.length === 0) { - return 0; - } - - const packagesToUpdate: string[] = []; - - this.logger.info('Fetching dependency metadata from registry...'); - 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, this.logger); - } catch (e) { - this.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') { - manifest = metadata.versions.get(requestIdentifier.fetchSpec); - } else if (requestIdentifier.type === 'range') { - const maxVersion = semver.maxSatisfying( - Array.from(metadata.versions.keys()), - requestIdentifier.fetchSpec, - ); - if (maxVersion) { - manifest = metadata.versions.get(maxVersion); - } - } else if (requestIdentifier.type === 'tag') { - manifest = metadata.tags[requestIdentifier.fetchSpec]; - } - - if (!manifest) { - this.logger.error( - `Package specified by '${requestIdentifier.raw}' does not exist within the registry.`, - ); - - return 1; - } - - if ( - (typeof node === 'string' && manifest.version === node) || - (typeof node === 'object' && manifest.version === node.package.version) - ) { - this.logger.info(`Package '${packageName}' is already up to date.`); - continue; - } - - packagesToUpdate.push(requestIdentifier.toString()); - } - - if (packagesToUpdate.length === 0) { - return 0; - } - - return this.runSchematic({ - collectionName: '@schematics/update', - schematicName: 'update', - dryRun: !!options.dryRun, - showNothingDone: false, - additionalOptions: { - force: options.force || false, - packageManager, - packages: packagesToUpdate, - }, - }); - } - - checkCleanGit() { - try { - 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.workspace.root), - path.resolve(entry.slice(3).trim()), - ); - - if (!relativeEntry.startsWith('..') && !path.isAbsolute(relativeEntry)) { - return false; - } - } - - } catch { } - - return true; - } -} diff --git a/packages/angular/cli/commands/update-long.md b/packages/angular/cli/commands/update-long.md deleted file mode 100644 index 2132f0ed69dd..000000000000 --- a/packages/angular/cli/commands/update-long.md +++ /dev/null @@ -1,9 +0,0 @@ -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=true` option. - -For detailed information and guidance on updating your application, see the interactive [Angular Update Guide](https://update.angular.io/). diff --git a/packages/angular/cli/commands/update.json b/packages/angular/cli/commands/update.json deleted file mode 100644 index d17d7c0fafb6..000000000000 --- a/packages/angular/cli/commands/update.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/update.json", - "description": "Updates your application and its dependencies. See https://update.angular.io/", - "$longDescription": "./update-long.md", - - "$scope": "all", - "$aliases": [], - "$type": "schematics", - "$impl": "./update-impl#UpdateCommand", - - "type": "object", - "allOf": [ - { - "$ref": "./definitions.json#/definitions/base" - }, - { - "type": "object", - "properties": { - "packages": { - "description": "The names of package(s) to update.", - "type": "array", - "items": { - "type": "string" - }, - "$default": { - "$source": "argv" - } - }, - "force": { - "description": "If false, will error out if installed packages are incompatible with the update.", - "default": false, - "type": "boolean" - }, - "all": { - "description": "Whether to update all packages in package.json.", - "default": false, - "type": "boolean" - }, - "next": { - "description": "Use the largest version, including beta and RCs.", - "default": false, - "type": "boolean" - }, - "migrateOnly": { - "description": "Only perform a migration, does not update the installed version.", - "type": "boolean" - }, - "from": { - "description": "Version from which to migrate from. Only available with a single package being updated, and only on migration only.", - "type": "string" - }, - "to": { - "description": "Version up to which to apply migrations. Only available with a single package being updated, and only on migrations only. Requires from to be specified. Default to the installed version detected.", - "type": "string" - }, - "allowDirty": { - "description": "Whether to allow updating when the repository contains modified or untracked files.", - "type": "boolean" - } - } - } - ] -} diff --git a/packages/angular/cli/commands/version-impl.ts b/packages/angular/cli/commands/version-impl.ts deleted file mode 100644 index acf6d0d5bdb0..000000000000 --- a/packages/angular/cli/commands/version-impl.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as child_process from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import { Command } from '../models/command'; -import { colors } from '../utilities/color'; -import { findUp } from '../utilities/find-up'; -import { Schema as VersionCommandSchema } from './version'; - -export class VersionCommand extends Command { - public static aliases = ['v']; - - async run() { - const pkg = require(path.resolve(__dirname, '..', 'package.json')); - let projPkg; - try { - projPkg = require(path.resolve(this.workspace.root, 'package.json')); - } catch (exception) { - projPkg = undefined; - } - - const patterns = [ - /^@angular\/.*/, - /^@angular-devkit\/.*/, - /^@bazel\/.*/, - /^@ngtools\/.*/, - /^@nguniversal\/.*/, - /^@schematics\/.*/, - /^rxjs$/, - /^typescript$/, - /^ng-packagr$/, - /^webpack$/, - ]; - - const maybeNodeModules = findUp('node_modules', __dirname); - const packageRoot = projPkg - ? path.resolve(this.workspace.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 rev-parse --abbrev-ref HEAD', { - cwd: __dirname, - encoding: 'utf8', - stdio: 'pipe', - }); - gitBranch = 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 => colors.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/version.json b/packages/angular/cli/commands/version.json deleted file mode 100644 index 795eb654b7a5..000000000000 --- a/packages/angular/cli/commands/version.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/version.json", - "description": "Outputs Angular CLI version.", - "$longDescription": "", - - "$aliases": [ "v" ], - "$scope": "all", - "$impl": "./version-impl#VersionCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/xi18n-impl.ts b/packages/angular/cli/commands/xi18n-impl.ts deleted file mode 100644 index 5412750138df..000000000000 --- a/packages/angular/cli/commands/xi18n-impl.ts +++ /dev/null @@ -1,29 +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 } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as Xi18nCommandSchema } from './xi18n'; - -export class Xi18nCommand extends ArchitectCommand { - public readonly target = 'extract-i18n'; - public readonly multiTarget: true; - - public async run(options: Xi18nCommandSchema & Arguments) { - const version = process.version.substr(1).split('.'); - if (Number(version[0]) === 12 && Number(version[1]) === 0) { - this.logger.error( - 'Due to a defect in Node.js 12.0, the command is not supported on this Node.js version. ' - + 'Please upgrade to Node.js 12.1 or later.'); - - return 1; - } - - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/xi18n.json b/packages/angular/cli/commands/xi18n.json deleted file mode 100644 index 082ae7ca1ba2..000000000000 --- a/packages/angular/cli/commands/xi18n.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/xi18n.json", - "description": "Extracts i18n messages from source code.", - "$longDescription": "", - - "$aliases": [], - "$scope": "in", - "$type": "architect", - "$impl": "./xi18n-impl#Xi18nCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index c5bb1c53c546..a2566853dfc7 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -1,83 +1,99 @@ /** * @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 { createConsoleLogger } from '@angular-devkit/core/node'; -import { normalize } from 'path'; -import { format } from 'util'; -import { runCommand } from '../../models/command-runner'; -import { colors, supportsColor } from '../../utilities/color'; -import { getWorkspaceRaw } from '../../utilities/config'; -import { getWorkspaceDetails } from '../../utilities/project'; -// tslint:disable: no-console -export default async function(options: { testing?: boolean; cliArgs: string[] }) { - const logger = createConsoleLogger(false, process.stdout, process.stderr, { - info: s => (supportsColor ? s : colors.unstyle(s)), - debug: s => (supportsColor ? s : colors.unstyle(s)), - warn: s => (supportsColor ? colors.bold.yellow(s) : colors.unstyle(s)), - error: s => (supportsColor ? colors.bold.red(s) : colors.unstyle(s)), - fatal: s => (supportsColor ? colors.bold.red(s) : colors.unstyle(s)), - }); +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'; - // Redirect console to logger - console.log = function() { - logger.info(format.apply(null, arguments)); - }; - console.info = function() { - logger.info(format.apply(null, arguments)); - }; - console.warn = function() { - logger.warn(format.apply(null, arguments)); - }; - console.error = function() { - logger.error(format.apply(null, arguments)); - }; +export { VERSION } from '../../src/utilities/version'; - let projectDetails = getWorkspaceDetails(); - if (projectDetails === null) { - const [, localPath] = getWorkspaceRaw('local'); - if (localPath !== null) { - logger.fatal( - `An invalid configuration file was found ['${localPath}'].` + - ' Please delete the file before running the command.', - ); +const MIN_NODEJS_VERSION = [20, 11] as const; - return 1; - } +/* 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', + ); - projectDetails = { root: process.cwd() }; + return 3; } - try { - const maybeExitCode = await runCommand(options.cliArgs, logger, projectDetails); - if (typeof maybeExitCode === 'number') { - console.assert(Number.isInteger(maybeExitCode)); + 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)), + }; + 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; + } - return maybeExitCode; + 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; } + }); - return 0; + // 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 { + return await runCommand(options.cliArgs, logger); } catch (err) { - if (err instanceof Error) { + if (err instanceof CommandModuleError) { + logger.fatal(`Error: ${err.message}`); + } else if (err instanceof Error) { try { - const fs = await import('fs'); - const os = await import('os'); - const tempDirectory = fs.mkdtempSync(fs.realpathSync(os.tmpdir()) + '/' + 'ng-'); - const logPath = normalize(tempDirectory + '/angular-errors.log'); - fs.appendFileSync(logPath, '[error] ' + (err.stack || err)); - + const logPath = writeErrorToLogFile(err); logger.fatal( `An unhandled exception occurred: ${err.message}\n` + - `See "${logPath}" for further details.` + `See "${logPath}" for further details.`, ); } catch (e) { logger.fatal( `An unhandled exception occurred: ${err.message}\n` + - `Fatal error writing debug log file: ${e.message}`, + `Fatal error writing debug log file: ${e}`, ); if (err.stack) { logger.fatal(err.stack); @@ -90,15 +106,12 @@ export default async function(options: { testing?: boolean; cliArgs: string[] }) } else if (typeof err === 'number') { // Log nothing. } else { - logger.fatal('An unexpected error occurred: ' + JSON.stringify(err)); - } - - if (options.testing) { - // tslint:disable-next-line: no-debugger - debugger; - throw err; + logger.fatal(`An unexpected error occurred: ${err}`); } return 1; + } finally { + logger.complete(); + await loggerFinished; } } diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json deleted file mode 100644 index 8a3117b98f1d..000000000000 --- a/packages/angular/cli/lib/config/schema.json +++ /dev/null @@ -1,1938 +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", "pnpm" ] - }, - "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", - "x-deprecated": true - } - } - }, - "analytics": { - "type": "boolean", - "description": "Share anonymous usage data with the Angular Team at Google." - } - }, - "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 directory 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" - }, - "style": { - "description": "The file extension or preprocessor to use for style files.", - "type": "string", - "default": "css", - "enum": [ - "css", - "scss", - "sass", - "less", - "styl" - ] - }, - "viewEncapsulation": { - "description": "Specifies the view encapsulation strategy.", - "enum": ["Emulated", "Native", "None", "ShadowDom"], - "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 directory 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 - }, - "skipTests": { - "type": "boolean", - "description": "When true, does not create test files.", - "default": false - } - } - }, - "@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" - }, - "flat": { - "type": "boolean", - "description": "Flag to indicate if a directory 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 directory is created." - }, - "spec": { - "type": "boolean", - "description": "Specifies if a spec file is generated.", - "default": true - }, - "skipTests": { - "type": "boolean", - "description": "When true, does not create test files.", - "default": false - } - } - }, - "@schematics/angular:pipe": { - "type": "object", - "properties": { - "flat": { - "type": "boolean", - "default": true, - "description": "Flag to indicate if a directory is created." - }, - "spec": { - "type": "boolean", - "description": "Specifies if a spec file is generated.", - "default": true - }, - "skipTests": { - "type": "boolean", - "description": "When true, does not create test files.", - "default": false - }, - "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", - "description": "Specifies if a spec file is generated.", - "default": true - }, - "skipTests": { - "type": "boolean", - "description": "When true, does not create test files.", - "default": false - } - } - } - }, - "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" - } - }, - "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": { - "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 Architect.", - "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 Architect.", - "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": { - "description": "Enables optimization of the build output.", - "oneOf": [ - { - "type": "object", - "properties": { - "scripts": { - "type": "boolean", - "description": "Enables optimization of the scripts output.", - "default": true - }, - "styles": { - "type": "boolean", - "description": "Enables optimization of the styles output.", - "default": true - } - }, - "additionalProperties": false - }, - { - "type": "boolean" - } - ] - }, - "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." - }, - "resourcesOutputPath": { - "type": "string", - "description": "The path where style resources will be placed, relative to outputPath." - }, - "aot": { - "type": "boolean", - "description": "Build using Ahead of Time compilation.", - "default": false - }, - "sourceMap": { - "description": "Output sourcemaps.", - "default": true, - "oneOf": [ - { - "type": "object", - "properties": { - "scripts": { - "type": "boolean", - "description": "Output sourcemaps for all scripts.", - "default": true - }, - "styles": { - "type": "boolean", - "description": "Output sourcemaps for all styles.", - "default": true - }, - "hidden": { - "type": "boolean", - "description": "Output sourcemaps used for error reporting tools.", - "default": false - }, - "vendor": { - "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", - "default": false - } - }, - "additionalProperties": false - }, - { - "type": "boolean" - } - ] - }, - "vendorSourceMap": { - "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", - "default": false - }, - "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 - }, - "ngswConfigPath": { - "type": "string", - "description": "Path to ngsw-config.json." - }, - "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'.", - "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": [] - }, - "es5BrowserSupport": { - "description": "Enables conditionally loaded ES2015 polyfills.", - "type": "boolean", - "default": false - }, - "rebaseRootRelativeCssUrls": { - "description": "Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only for compatibility and transition. The behavior of this option is non-standard and will be removed in the next major release.", - "type": "boolean", - "default": false, - "x-deprecated": true - }, - "webWorkerTsConfig": { - "type": "string", - "description": "TypeScript configuration for Web Worker modules." - }, - "crossOrigin": { - "type": "string", - "description": "Define the crossorigin attribute setting of elements that provide CORS support.", - "default": "none", - "enum": [ - "none", - "anonymous", - "use-credentials" - ] - } - }, - "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." - }, - "ignore": { - "description": "An array of globs to ignore.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "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 - }, - "inject": { - "type": "boolean", - "description": "If the bundle will be referenced in the HTML file.", - "default": true - } - }, - "additionalProperties": false, - "required": [ - "input" - ] - }, - { - "type": "string", - "description": "The file to include." - } - ] - }, - "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 Architect.", - "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": "When true, open the live-reload URL in default browser.", - "default": false, - "alias": "o" - }, - "liveReload": { - "type": "boolean", - "description": "When true, reload the page on change using live-reload.", - "default": true - }, - "publicHost": { - "type": "string", - "description": "The URL that the browser client (or live-reload client, if enabled) should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies." - }, - "servePath": { - "type": "string", - "description": "The pathname where the app will be served." - }, - "disableHostCheck": { - "type": "boolean", - "description": "When true, don't verify that connected clients are part of allowed hosts.", - "default": false - }, - "hmr": { - "type": "boolean", - "description": "When true, enable hot module replacement.", - "default": false - }, - "watch": { - "type": "boolean", - "description": "When true, rebuild on change.", - "default": true - }, - "hmrWarning": { - "type": "boolean", - "description": "When true, show a warning when the --hmr option is enabled.", - "default": true - }, - "servePathDefaultWarning": { - "type": "boolean", - "description": "When true, show a warning when deploy-url/base-href use unsupported serve path values.", - "default": true - }, - "optimization": { - "description": "Enable optimization of the build output.", - "oneOf": [ - { - "type": "object", - "properties": { - "scripts": { - "type": "boolean", - "description": "When true, enable optimization of the scripts output.", - "default": true - }, - "styles": { - "type": "boolean", - "description": "When true, enable optimization of the styles output.", - "default": true - } - }, - "additionalProperties": false - }, - { - "type": "boolean" - } - ] - }, - "aot": { - "type": "boolean", - "description": "Build using ahead-of-time compilation." - }, - "sourceMap": { - "description": "When true, output sourcemaps.", - "default": true, - "oneOf": [ - { - "type": "object", - "properties": { - "scripts": { - "type": "boolean", - "description": "When true, output sourcemaps for all scripts.", - "default": true - }, - "styles": { - "type": "boolean", - "description": "When true, output sourcemaps for all styles.", - "default": true - }, - "vendor": { - "type": "boolean", - "description": "When true, resolve vendor packages sourcemaps.", - "default": false - } - }, - "additionalProperties": false - }, - { - "type": "boolean" - } - ] - }, - "vendorSourceMap": { - "type": "boolean", - "description": "When true, resolve vendor packages sourcemaps.", - "default": false - }, - "evalSourceMap": { - "type": "boolean", - "description": "When true, output in-file eval sourcemaps." - }, - "vendorChunk": { - "type": "boolean", - "description": "When true, use a separate bundle containing only vendor libraries." - }, - "commonChunk": { - "type": "boolean", - "description": "When true, 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": "When true, add more details to output logging." - }, - "progress": { - "type": "boolean", - "description": "When true, log progress to the console while building." - } - }, - "additionalProperties": false - }, - "extracti18n": { - "description": "Extract i18n target options for Architect.", - "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." - }, - "progress": { - "type": "boolean", - "description": "Log progress to the console.", - "default": true - }, - "outputPath": { - "type": "string", - "description": "Path where output will be placed." - }, - "outFile": { - "type": "string", - "description": "Name of the file to output." - } - }, - "additionalProperties": false - }, - "karma": { - "description": "Karma target options for Architect.", - "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": { - "description": "Output sourcemaps.", - "default": true, - "oneOf": [ - { - "type": "object", - "properties": { - "scripts": { - "type": "boolean", - "description": "Output sourcemaps for all scripts.", - "default": true - }, - "styles": { - "type": "boolean", - "description": "Output sourcemaps for all styles.", - "default": true - }, - "vendor": { - "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", - "default": false - } - }, - "additionalProperties": false - }, - { - "type": "boolean" - } - ] - }, - "progress": { - "type": "boolean", - "description": "Log progress to the console while building.", - "default": true - }, - "watch": { - "type": "boolean", - "description": "Run build when files change.", - "default": true - }, - "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": [] - }, - "reporters": { - "type": "array", - "description": "Karma reporters to use. Directly passed to the karma runner.", - "items": { - "type": "string" - } - }, - "webWorkerTsConfig": { - "type": "string", - "description": "TypeScript configuration for Web Worker modules." - } - }, - "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." - }, - "ignore": { - "description": "An array of globs to ignore.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "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 - }, - "inject": { - "type": "boolean", - "description": "If the bundle will be referenced in the HTML file.", - "default": true - } - }, - "additionalProperties": false, - "required": [ - "input" - ] - }, - { - "type": "string", - "description": "The file to include." - } - ] - } - } - }, - "protractor": { - "description": "Protractor target options for Architect.", - "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": { - "description": "Enables optimization of the build output.", - "oneOf": [ - { - "type": "object", - "properties": { - "scripts": { - "type": "boolean", - "description": "Enables optimization of the scripts output.", - "default": true - }, - "styles": { - "type": "boolean", - "description": "Enables optimization of the styles output.", - "default": true - } - }, - "additionalProperties": false - }, - { - "type": "boolean" - } - ] - }, - "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." - }, - "resourcesOutputPath": { - "type": "string", - "description": "The path where style resources will be placed, relative to outputPath." - }, - "sourceMap": { - "description": "Output sourcemaps.", - "default": true, - "oneOf": [ - { - "type": "object", - "properties": { - "scripts": { - "type": "boolean", - "description": "Output sourcemaps for all scripts.", - "default": true - }, - "styles": { - "type": "boolean", - "description": "Output sourcemaps for all styles.", - "default": true - }, - "hidden": { - "type": "boolean", - "description": "Output sourcemaps used for error reporting tools.", - "default": false - }, - "vendor": { - "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", - "default": false - } - }, - "additionalProperties": false - }, - { - "type": "boolean" - } - ] - }, - "vendorSourceMap": { - "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", - "default": false - }, - "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'.", - "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 Architect.", - "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 c1d3d71aa3c2..cd324b6df69b 100644 --- a/packages/angular/cli/lib/init.ts +++ b/packages/angular/cli/lib/init.ts @@ -1,152 +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: no-console -// tslint:disable-next-line:ordered-imports import-groups -import { tags } from '@angular-devkit/core'; -import * as fs from 'fs'; -import * as path from 'path'; -import { SemVer } from 'semver'; -import { Duplex } from 'stream'; -import { colors } from '../utilities/color'; -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(); - - 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']); - } - } - } - - // Check the parent. - cwd = path.dirname(cwd); - } while (cwd != path.dirname(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; + } - return null; -} + let cli; -// Check if we need to profile this CLI run. -if (process.env['NG_CLI_PROFILING']) { - let profiler: { - startProfiling: (name?: string, recsamples?: boolean) => void; - stopProfiling: (name?: string) => any; // tslint:disable-line:no-any - }; try { - profiler = require('v8-profiler-node8'); // tslint:disable-line:no-implicit-dependencies - } catch (err) { - throw new Error( - `Could not require 'v8-profiler-node8'. You must install it separetely with ` + - `'npm install v8-profiler-node8 --no-save'.\n\nOriginal error:\n\n${err}`, - ); - } + // 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); + } + } - profiler.startProfiling(); + // Ensure older versions of the CLI fully exit + const localMajorVersion = major(localVersion); + if (localMajorVersion > 0 && localMajorVersion < 14) { + forceExit = true; - 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 = require.resolve('@angular/cli', { paths: [process.cwd()] }); - - // 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 = colors.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.error(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']; -} - -// This is required to support 1.x local versions with a 6+ global -let standardInput; -try { - standardInput = process.stdin; -} catch (e) { - delete process.stdin; - process.stdin = new Duplex(); - standardInput = process.stdin; -} + if ('default' in cli) { + cli = cli['default']; + } -cli({ - cliArgs: process.argv.slice(2), - inputStream: standardInput, - outputStream: process.stdout, -}) - .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) => { + // eslint-disable-next-line no-console console.error('Unknown error: ' + err.toString()); process.exit(127); }); diff --git a/packages/angular/cli/models/analytics.ts b/packages/angular/cli/models/analytics.ts deleted file mode 100644 index ae4bc8b65cdc..000000000000 --- a/packages/angular/cli/models/analytics.ts +++ /dev/null @@ -1,592 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { analytics, json, tags } from '@angular-devkit/core'; -import * as child_process from 'child_process'; -import * as debug from 'debug'; -import { writeFileSync } from 'fs'; -import * as inquirer from 'inquirer'; -import * as os from 'os'; -import * as ua from 'universal-analytics'; -import { v4 as uuidV4 } from 'uuid'; -import { colors } from '../utilities/color'; -import { getWorkspace, getWorkspaceRaw } from '../utilities/config'; -import { isTTY } from '../utilities/tty'; - -// tslint:disable: no-console -const analyticsDebug = debug('ng:analytics'); // Generate analytics, including settings and users. -const analyticsLogDebug = debug('ng:analytics:log'); // Actual logs of events. - -const BYTES_PER_GIGABYTES = 1024 * 1024 * 1024; - -let _defaultAngularCliPropertyCache: string; -export const AnalyticsProperties = { - AngularCliProd: 'UA-8594346-29', - AngularCliStaging: 'UA-8594346-32', - get AngularCliDefault(): string { - if (_defaultAngularCliPropertyCache) { - return _defaultAngularCliPropertyCache; - } - - const v = require('../package.json').version; - - // The logic is if it's a full version then we should use the prod GA property. - if (/^\d+\.\d+\.\d+$/.test(v) && v !== '0.0.0') { - _defaultAngularCliPropertyCache = AnalyticsProperties.AngularCliProd; - } else { - _defaultAngularCliPropertyCache = AnalyticsProperties.AngularCliStaging; - } - - return _defaultAngularCliPropertyCache; - }, -}; - -/** - * This is the ultimate safelist for checking if a package name is safe to report to analytics. - */ -export const analyticsPackageSafelist = [ - /^@angular\//, - /^@angular-devkit\//, - /^@ngtools\//, - '@schematics/angular', - '@schematics/schematics', - '@schematics/update', -]; - -export function isPackageNameSafeForAnalytics(name: string) { - return analyticsPackageSafelist.some(pattern => { - if (typeof pattern == 'string') { - return pattern === name; - } else { - return pattern.test(name); - } - }); -} - -/** - * Attempt to get the Windows Language Code string. - * @private - */ -function _getWindowsLanguageCode(): string | undefined { - if (!os.platform().startsWith('win')) { - return undefined; - } - - try { - // This is true on Windows XP, 7, 8 and 10 AFAIK. Would return empty string or fail if it - // doesn't work. - return child_process - .execSync('wmic.exe os get locale') - .toString() - .trim(); - } catch (_) {} - - return undefined; -} - -/** - * Get a language code. - * @private - */ -function _getLanguage() { - // Note: Windows does not expose the configured language by default. - return ( - process.env.LANG || // Default Unix env variable. - process.env.LC_CTYPE || // For C libraries. Sometimes the above isn't set. - process.env.LANGSPEC || // For Windows, sometimes this will be set (not always). - _getWindowsLanguageCode() || - '??' - ); // ¯\_(ツ)_/¯ -} - -/** - * Return the number of CPUs. - * @private - */ -function _getCpuCount() { - const cpus = os.cpus(); - - // Return "(count)x(average speed)". - return cpus.length; -} - -/** - * Get the first CPU's speed. It's very rare to have multiple CPUs of different speed (in most - * non-ARM configurations anyway), so that's all we care about. - * @private - */ -function _getCpuSpeed() { - const cpus = os.cpus(); - - return Math.floor(cpus[0].speed); -} - -/** - * Get the amount of memory, in megabytes. - * @private - */ -function _getRamSize() { - // Report in gigabytes (or closest). Otherwise it's too much noise. - return Math.round(os.totalmem() / BYTES_PER_GIGABYTES); -} - -/** - * Get the Node name and version. This returns a string like "Node 10.11", or "io.js 3.5". - * @private - */ -function _getNodeVersion() { - // We use any here because p.release is a new Node construct in Node 10 (and our typings are the - // minimal version of Node we support). - const p = process as any; // tslint:disable-line:no-any - const name = - (typeof p.release == 'object' && typeof p.release.name == 'string' && p.release.name) || - process.argv0; - - return name + ' ' + process.version; -} - -/** - * Get a numerical MAJOR.MINOR version of node. We report this as a metric. - * @private - */ -function _getNumericNodeVersion() { - const p = process.version; - const m = p.match(/\d+\.\d+/); - - return (m && m[0] && parseFloat(m[0])) || 0; -} - -// These are just approximations of UA strings. We just try to fool Google Analytics to give us the -// data we want. -// See https://developers.whatismybrowser.com/useragents/ -const osVersionMap: { [os: string]: { [release: string]: string } } = { - darwin: { - '1.3.1': '10_0_4', - '1.4.1': '10_1_0', - '5.1': '10_1_1', - '5.2': '10_1_5', - '6.0.1': '10_2', - '6.8': '10_2_8', - '7.0': '10_3_0', - '7.9': '10_3_9', - '8.0': '10_4_0', - '8.11': '10_4_11', - '9.0': '10_5_0', - '9.8': '10_5_8', - '10.0': '10_6_0', - '10.8': '10_6_8', - // We stop here because we try to math out the version for anything greater than 10, and it - // works. Those versions are standardized using a calculation now. - }, - win32: { - '6.3.9600': 'Windows 8.1', - '6.2.9200': 'Windows 8', - '6.1.7601': 'Windows 7 SP1', - '6.1.7600': 'Windows 7', - '6.0.6002': 'Windows Vista SP2', - '6.0.6000': 'Windows Vista', - '5.1.2600': 'Windows XP', - }, -}; - -/** - * Build a fake User Agent string for OSX. This gets sent to Analytics so it shows the proper OS, - * versions and others. - * @private - */ -function _buildUserAgentStringForOsx() { - let v = osVersionMap.darwin[os.release()]; - - if (!v) { - // Remove 4 to tie Darwin version to OSX version, add other info. - const x = parseFloat(os.release()); - if (x > 10) { - v = `10_` + (x - 4).toString().replace('.', '_'); - } - } - - const cpuModel = os.cpus()[0].model.match(/^[a-z]+/i); - const cpu = cpuModel ? cpuModel[0] : os.cpus()[0].model; - - return `(Macintosh; ${cpu} Mac OS X ${v || os.release()})`; -} - -/** - * Build a fake User Agent string for Windows. This gets sent to Analytics so it shows the proper - * OS, versions and others. - * @private - */ -function _buildUserAgentStringForWindows() { - return `(Windows NT ${os.release()})`; -} - -/** - * Build a fake User Agent string for Linux. This gets sent to Analytics so it shows the proper OS, - * versions and others. - * @private - */ -function _buildUserAgentStringForLinux() { - return `(X11; Linux i686; ${os.release()}; ${os.cpus()[0].model})`; -} - -/** - * Build a fake User Agent string. This gets sent to Analytics so it shows the proper OS version. - * @private - */ -function _buildUserAgentString() { - switch (os.platform()) { - case 'darwin': - return _buildUserAgentStringForOsx(); - - case 'win32': - return _buildUserAgentStringForWindows(); - - case 'linux': - return _buildUserAgentStringForLinux(); - - default: - return os.platform() + ' ' + os.release(); - } -} - -/** - * Implementation of the Analytics interface for using `universal-analytics` package. - */ -export class UniversalAnalytics implements analytics.Analytics { - private _ua: ua.Visitor; - private _dirty = false; - private _metrics: (string | number)[] = []; - private _dimensions: (string | number)[] = []; - - /** - * @param trackingId The Google Analytics ID. - * @param uid A User ID. - */ - constructor(trackingId: string, uid: string) { - this._ua = ua(trackingId, uid, { - enableBatching: true, - batchSize: 5, - }); - - // Add persistent params for appVersion. - this._ua.set('ds', 'cli'); - this._ua.set('ua', _buildUserAgentString()); - this._ua.set('ul', _getLanguage()); - - // @angular/cli with version. - this._ua.set('an', require('../package.json').name); - this._ua.set('av', require('../package.json').version); - - // We use the application ID for the Node version. This should be "node 10.10.0". - // We also use a custom metrics, but - this._ua.set('aid', _getNodeVersion()); - - // We set custom metrics for values we care about. - this._dimensions[analytics.NgCliAnalyticsDimensions.CpuCount] = _getCpuCount(); - this._dimensions[analytics.NgCliAnalyticsDimensions.CpuSpeed] = _getCpuSpeed(); - this._dimensions[analytics.NgCliAnalyticsDimensions.RamInGigabytes] = _getRamSize(); - this._dimensions[analytics.NgCliAnalyticsDimensions.NodeVersion] = _getNumericNodeVersion(); - } - - /** - * Creates the dimension and metrics variables to pass to universal-analytics. - * @private - */ - private _customVariables(options: analytics.CustomDimensionsAndMetricsOptions) { - const additionals: { [key: string]: boolean | number | string } = {}; - this._dimensions.forEach((v, i) => (additionals['cd' + i] = v)); - (options.dimensions || []).forEach((v, i) => (additionals['cd' + i] = v)); - this._metrics.forEach((v, i) => (additionals['cm' + i] = v)); - (options.metrics || []).forEach((v, i) => (additionals['cm' + i] = v)); - - return additionals; - } - - event(ec: string, ea: string, options: analytics.EventOptions = {}) { - const vars = this._customVariables(options); - analyticsLogDebug('event ec=%j, ea=%j, %j', ec, ea, vars); - - const { label: el, value: ev } = options; - this._dirty = true; - this._ua.event({ ec, ea, el, ev, ...vars }); - } - screenview(cd: string, an: string, options: analytics.ScreenviewOptions = {}) { - const vars = this._customVariables(options); - analyticsLogDebug('screenview cd=%j, an=%j, %j', cd, an, vars); - - const { appVersion: av, appId: aid, appInstallerId: aiid } = options; - this._dirty = true; - this._ua.screenview({ cd, an, av, aid, aiid, ...vars }); - } - pageview(dp: string, options: analytics.PageviewOptions = {}) { - const vars = this._customVariables(options); - analyticsLogDebug('pageview dp=%j, %j', dp, vars); - - const { hostname: dh, title: dt } = options; - this._dirty = true; - this._ua.pageview({ dp, dh, dt, ...vars }); - } - timing(utc: string, utv: string, utt: string | number, options: analytics.TimingOptions = {}) { - const vars = this._customVariables(options); - analyticsLogDebug('timing utc=%j, utv=%j, utl=%j, %j', utc, utv, utt, vars); - - const { label: utl } = options; - this._dirty = true; - this._ua.timing({ utc, utv, utt, utl, ...vars }); - } - - flush(): Promise { - if (!this._dirty) { - return Promise.resolve(); - } - - this._dirty = false; - - return new Promise(resolve => this._ua.send(resolve)); - } -} - -/** - * Set analytics settings. This does not work if the user is not inside a project. - * @param level 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 function setAnalyticsConfig(level: 'global' | 'local', value: string | boolean) { - analyticsDebug('setting %s level analytics to: %s', level, value); - const [config, configPath] = getWorkspaceRaw(level); - if (!config || !configPath) { - throw new Error(`Could not find ${level} workspace.`); - } - - const configValue = config.value; - const cli: json.JsonValue = configValue['cli'] || (configValue['cli'] = {}); - - if (!json.isJsonObject(cli)) { - throw new Error(`Invalid config found at ${configPath}. CLI should be an object.`); - } - - if (value === true) { - value = uuidV4(); - } - cli['analytics'] = value; - - const output = JSON.stringify(configValue, null, 2); - writeFileSync(configPath, output); - analyticsDebug('done'); -} - -/** - * 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 promptGlobalAnalytics(force = false) { - analyticsDebug('prompting global analytics.'); - if (force || isTTY()) { - const answers = await inquirer.prompt<{ analytics: boolean }>([ - { - type: 'confirm', - name: 'analytics', - message: tags.stripIndents` - Would you like to share anonymous usage data 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 http://angular.io/analytics. - `, - default: false, - }, - ]); - - setAnalyticsConfig('global', answers.analytics); - - if (answers.analytics) { - console.log(''); - console.log(tags.stripIndent` - Thank you for sharing anonymous usage data. If you change your mind, the following - command will disable this feature entirely: - - ${colors.yellow('ng analytics off')} - `); - console.log(''); - } else { - // Send back a ping with the user `optout`. This is the only thing we send. - const ua = new UniversalAnalytics(AnalyticsProperties.AngularCliDefault, 'optout'); - ua.pageview('/telemetry/optout'); - await ua.flush(); - } - - return true; - } else { - analyticsDebug('Either STDOUT or STDIN are not TTY and we skipped the prompt.'); - } - - return false; -} - -/** - * Prompt the user for usage gathering permission for the local project. Fails if there is no - * local workspace. - * @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 promptProjectAnalytics(force = false): Promise { - analyticsDebug('prompting user'); - const [config, configPath] = getWorkspaceRaw('local'); - if (!config || !configPath) { - throw new Error(`Could not find a local workspace. Are you in a project?`); - } - - if (force || isTTY()) { - const answers = await inquirer.prompt<{ analytics: boolean }>([ - { - type: 'confirm', - name: 'analytics', - message: tags.stripIndents` - Would you like to share anonymous 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 http://angular.io/analytics. - - `, - default: false, - }, - ]); - - setAnalyticsConfig('local', answers.analytics); - - if (answers.analytics) { - console.log(''); - console.log(tags.stripIndent` - Thank you for sharing anonymous usage data. Would you change your mind, the following - command will disable this feature entirely: - - ${colors.yellow('ng analytics project off')} - `); - console.log(''); - } else { - // Send back a ping with the user `optout`. This is the only thing we send. - const ua = new UniversalAnalytics(AnalyticsProperties.AngularCliDefault, 'optout'); - ua.pageview('/telemetry/project/optout'); - await ua.flush(); - } - - return true; - } - - return false; -} - -export function hasGlobalAnalyticsConfiguration(): boolean { - try { - const globalWorkspace = getWorkspace('global'); - const analyticsConfig: string | undefined | null | { uid?: string } = - globalWorkspace && globalWorkspace.getCli() && globalWorkspace.getCli()['analytics']; - - if (analyticsConfig !== null && analyticsConfig !== undefined) { - return true; - } - } catch {} - - return false; -} - -/** - * Get the global analytics object for the user. This returns an instance of UniversalAnalytics, - * or undefined if analytics are disabled. - * - * If any problem happens, it is considered the user has been opting out of analytics. - */ -export function getGlobalAnalytics(): UniversalAnalytics | undefined { - analyticsDebug('getGlobalAnalytics'); - const propertyId = AnalyticsProperties.AngularCliDefault; - - if ('NG_CLI_ANALYTICS' in process.env) { - if (process.env['NG_CLI_ANALYTICS'] == 'false' || process.env['NG_CLI_ANALYTICS'] == '') { - analyticsDebug('NG_CLI_ANALYTICS is false'); - - return undefined; - } - if (process.env['NG_CLI_ANALYTICS'] === 'ci') { - analyticsDebug('Running in CI mode'); - - return new UniversalAnalytics(propertyId, 'ci'); - } - } - - // If anything happens we just keep the NOOP analytics. - try { - const globalWorkspace = getWorkspace('global'); - const analyticsConfig: string | undefined | null | { uid?: string } = - globalWorkspace && globalWorkspace.getCli() && globalWorkspace.getCli()['analytics']; - analyticsDebug('Client Analytics config found: %j', analyticsConfig); - - if (analyticsConfig === false) { - analyticsDebug('Analytics disabled. Ignoring all analytics.'); - - return undefined; - } else if (analyticsConfig === undefined || analyticsConfig === null) { - analyticsDebug('Analytics settings not found. Ignoring all analytics.'); - - // globalWorkspace can be null if there is no file. analyticsConfig would be null in this - // case. Since there is no file, the user hasn't answered and the expected return value is - // undefined. - return undefined; - } else { - let uid: string | undefined = undefined; - if (typeof analyticsConfig == 'string') { - uid = analyticsConfig; - } else if (typeof analyticsConfig == 'object' && typeof analyticsConfig['uid'] == 'string') { - uid = analyticsConfig['uid']; - } - - analyticsDebug('client id: %j', uid); - if (uid == undefined) { - return undefined; - } - - return new UniversalAnalytics(propertyId, uid); - } - } catch (err) { - analyticsDebug('Error happened during reading of analytics config: %s', err.message); - - return undefined; - } -} - -/** - * Return the usage analytics sharing setting, which is either a property string (GA-XXXXXXX-XX), - * or undefined if no sharing. - */ -export function getSharedAnalytics(): UniversalAnalytics | undefined { - analyticsDebug('getSharedAnalytics'); - - const envVarName = 'NG_CLI_ANALYTICS_SHARE'; - if (envVarName in process.env) { - if (process.env[envVarName] == 'false' || process.env[envVarName] == '') { - analyticsDebug('NG_CLI_ANALYTICS is false'); - - return undefined; - } - } - - // If anything happens we just keep the NOOP analytics. - try { - const globalWorkspace = getWorkspace('global'); - const analyticsConfig = - globalWorkspace && globalWorkspace.getCli() && globalWorkspace.getCli()['analyticsSharing']; - - if (!analyticsConfig || !analyticsConfig.tracking || !analyticsConfig.uuid) { - return undefined; - } else { - analyticsDebug('Analytics sharing info: %j', analyticsConfig); - - return new UniversalAnalytics(analyticsConfig.tracking, analyticsConfig.uuid); - } - } catch (err) { - analyticsDebug('Error happened during reading of analytics sharing config: %s', err.message); - - return undefined; - } -} diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts deleted file mode 100644 index 9af118b80db1..000000000000 --- a/packages/angular/cli/models/architect-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 - */ -import { Architect, Target } from '@angular-devkit/architect'; -import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; -import { experimental, json, schema, tags } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { BepJsonWriter } from '../utilities/bep'; -import { parseJsonSchemaToOptions } from '../utilities/json-schema'; -import { isPackageNameSafeForAnalytics } from './analytics'; -import { BaseCommandOptions, Command } from './command'; -import { Arguments, Option } from './interface'; -import { parseArguments } from './parser'; -import { WorkspaceLoader } from './workspace-loader'; - -export interface ArchitectCommandOptions extends BaseCommandOptions { - project?: string; - configuration?: string; - prod?: boolean; - target?: string; -} - -export abstract class ArchitectCommand< - T extends ArchitectCommandOptions = ArchitectCommandOptions, -> extends Command { - protected _architect: Architect; - protected _architectHost: WorkspaceNodeModulesArchitectHost; - protected _workspace: experimental.workspace.Workspace; - protected _registry: json.schema.SchemaRegistry; - - // If this command supports running multiple targets. - protected multiTarget = false; - - target: string | undefined; - - public async initialize(options: ArchitectCommandOptions & Arguments): Promise { - await super.initialize(options); - - this._registry = new json.schema.CoreSchemaRegistry(); - this._registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); - - const workspaceLoader = new WorkspaceLoader(new NodeJsSyncHost()); - - const workspace = await workspaceLoader.loadWorkspace(this.workspace.root); - this._workspace = workspace; - - this._architectHost = new WorkspaceNodeModulesArchitectHost(workspace, this.workspace.root); - this._architect = new Architect(this._architectHost, this._registry); - - if (!this.target) { - if (options.help) { - // This is a special case where we just return. - return; - } - - const specifier = this._makeTargetSpecifier(options); - if (!specifier.project || !specifier.target) { - throw new Error('Cannot determine project or target for command.'); - } - - return; - } - - const commandLeftovers = options['--']; - let projectName = options.project; - const targetProjectNames: string[] = []; - for (const name of this._workspace.listProjectNames()) { - if (this._workspace.getProjectTargets(name)[this.target]) { - targetProjectNames.push(name); - } - } - - if (targetProjectNames.length === 0) { - throw new Error(`No projects support the '${this.target}' target.`); - } - - if (projectName && !targetProjectNames.includes(projectName)) { - throw new Error(`Project '${projectName}' does not support the '${this.target}' target.`); - } - - if (!projectName && commandLeftovers && commandLeftovers.length > 0) { - const builderNames = new Set(); - const leftoverMap = new Map(); - let potentialProjectNames = new Set(targetProjectNames); - for (const name of targetProjectNames) { - const builderName = await this._architectHost.getBuilderNameForTarget({ - project: name, - target: this.target, - }); - - if (this.multiTarget) { - builderNames.add(builderName); - } - - const builderDesc = await this._architectHost.resolveBuilder(builderName); - const optionDefs = await parseJsonSchemaToOptions( - this._registry, - builderDesc.optionSchema as json.JsonObject, - ); - const parsedOptions = parseArguments([...commandLeftovers], optionDefs); - const builderLeftovers = parsedOptions['--'] || []; - leftoverMap.set(name, { optionDefs, parsedOptions }); - - potentialProjectNames = new Set(builderLeftovers.filter(x => potentialProjectNames.has(x))); - } - - if (potentialProjectNames.size === 1) { - projectName = [...potentialProjectNames][0]; - - // remove the project name from the leftovers - const optionInfo = leftoverMap.get(projectName); - if (optionInfo) { - const locations = []; - let i = 0; - while (i < commandLeftovers.length) { - i = commandLeftovers.indexOf(projectName, i + 1); - if (i === -1) { - break; - } - locations.push(i); - } - delete optionInfo.parsedOptions['--']; - for (const location of locations) { - const tempLeftovers = [...commandLeftovers]; - tempLeftovers.splice(location, 1); - const tempArgs = parseArguments([...tempLeftovers], optionInfo.optionDefs); - delete tempArgs['--']; - if (JSON.stringify(optionInfo.parsedOptions) === JSON.stringify(tempArgs)) { - options['--'] = tempLeftovers; - break; - } - } - } - } - - if (!projectName && this.multiTarget && builderNames.size > 1) { - throw new Error(tags.oneLine` - Architect commands with command line overrides cannot target different builders. The - '${this.target}' target would run on projects ${targetProjectNames.join()} which have the - following builders: ${'\n ' + [...builderNames].join('\n ')} - `); - } - } - - if (!projectName && !this.multiTarget) { - const defaultProjectName = this._workspace.getDefaultProjectName(); - if (targetProjectNames.length === 1) { - projectName = targetProjectNames[0]; - } else if (defaultProjectName && targetProjectNames.includes(defaultProjectName)) { - projectName = defaultProjectName; - } else if (options.help) { - // This is a special case where we just return. - return; - } else { - throw new Error('Cannot determine project or target for command.'); - } - } - - options.project = projectName; - - const builderConf = await this._architectHost.getBuilderNameForTarget({ - project: projectName || (targetProjectNames.length > 0 ? targetProjectNames[0] : ''), - target: this.target, - }); - const builderDesc = await this._architectHost.resolveBuilder(builderConf); - - this.description.options.push(...( - await parseJsonSchemaToOptions(this._registry, builderDesc.optionSchema as json.JsonObject) - )); - - // Update options to remove analytics from options if the builder isn't safelisted. - for (const o of this.description.options) { - if (o.userAnalytics) { - if (!isPackageNameSafeForAnalytics(builderConf)) { - o.userAnalytics = undefined; - } - } - } - } - - async run(options: ArchitectCommandOptions & Arguments) { - return await this.runArchitectTarget(options); - } - - protected async runBepTarget( - command: string, - configuration: Target, - overrides: json.JsonObject, - buildEventLog: string, - ): Promise { - const bep = new BepJsonWriter(buildEventLog); - - // Send start - bep.writeBuildStarted(command); - - let last = 1; - let rebuild = false; - const run = await this._architect.scheduleTarget( - configuration, - overrides, - { logger: this.logger }, - ); - await run.output.forEach(event => { - last = event.success ? 0 : 1; - - if (rebuild) { - // NOTE: This will have an incorrect timestamp but this cannot be fixed - // until builders report additional status events - bep.writeBuildStarted(command); - } else { - rebuild = true; - } - - bep.writeBuildFinished(last); - }); - - await run.stop(); - - return last; - } - - protected async runSingleTarget( - target: Target, - targetOptions: string[], - commandOptions: ArchitectCommandOptions & Arguments, - ) { - // We need to build the builderSpec twice because architect does not understand - // overrides separately (getting the configuration builds the whole project, including - // overrides). - const builderConf = await this._architectHost.getBuilderNameForTarget(target); - const builderDesc = await this._architectHost.resolveBuilder(builderConf); - const targetOptionArray = await parseJsonSchemaToOptions( - this._registry, - builderDesc.optionSchema as json.JsonObject, - ); - const overrides = parseArguments(targetOptions, targetOptionArray, this.logger); - - const allowAdditionalProperties = typeof builderDesc.optionSchema === 'object' - && builderDesc.optionSchema.additionalProperties; - - if (overrides['--'] && !allowAdditionalProperties) { - (overrides['--'] || []).forEach(additional => { - this.logger.fatal(`Unknown option: '${additional.split(/=/)[0]}'`); - }); - - return 1; - } - - if (commandOptions.buildEventLog && ['build', 'serve'].includes(this.description.name)) { - // The build/serve commands supports BEP messaging - this.logger.warn('BEP support is experimental and subject to change.'); - - return this.runBepTarget( - this.description.name, - target, - overrides as json.JsonObject, - commandOptions.buildEventLog as string, - ); - } else { - const run = await this._architect.scheduleTarget( - target, - overrides as json.JsonObject, - { - logger: this.logger, - analytics: isPackageNameSafeForAnalytics(builderConf) ? this.analytics : undefined, - }, - ); - - const { error, success } = await run.output.toPromise(); - await run.stop(); - - if (error) { - this.logger.error(error); - } - - return success ? 0 : 1; - } - } - - protected async runArchitectTarget( - options: ArchitectCommandOptions & Arguments, - ): Promise { - const extra = options['--'] || []; - - try { - const targetSpec = this._makeTargetSpecifier(options); - if (!targetSpec.project && this.target) { - // This runs each target sequentially. - // Running them in parallel would jumble the log messages. - let result = 0; - for (const project of this.getProjectNamesByTarget(this.target)) { - result |= await this.runSingleTarget( - { ...targetSpec, project } as Target, - extra, - options, - ); - } - - return result; - } else { - return await this.runSingleTarget(targetSpec, extra, options); - } - } 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._workspace.getProjectTargets(projectName)[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 the default 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 _makeTargetSpecifier(commandOptions: ArchitectCommandOptions): Target { - let project, target, configuration; - - if (commandOptions.target) { - [project, target, configuration] = commandOptions.target.split(':'); - - if (commandOptions.configuration) { - configuration = commandOptions.configuration; - } - } else { - project = commandOptions.project; - target = this.target; - configuration = commandOptions.configuration; - if (!configuration && commandOptions.prod) { - configuration = 'production'; - } - } - - if (!project) { - project = ''; - } - if (!target) { - target = ''; - } - - return { - project, - configuration: configuration || '', - target, - }; - } -} diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts deleted file mode 100644 index 58387fcef775..000000000000 --- a/packages/angular/cli/models/command-runner.ts +++ /dev/null @@ -1,242 +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 { - JsonParseMode, - analytics, - isJsonObject, - json, - logging, - schema, - strings, - tags, -} from '@angular-devkit/core'; -import * as debug from 'debug'; -import { readFileSync } from 'fs'; -import { join, resolve } from 'path'; -import { parseJsonSchemaToCommandDescription } from '../utilities/json-schema'; -import { UniversalAnalytics, getGlobalAnalytics, getSharedAnalytics } from './analytics'; -import { Command } from './command'; -import { CommandDescription, CommandWorkspace } from './interface'; -import * as parser from './parser'; - -const analyticsDebug = debug('ng:analytics:commands'); - -// NOTE: Update commands.json if changing this. It's still deep imported in one CI validation -const standardCommands = { - 'add': '../commands/add.json', - 'analytics': '../commands/analytics.json', - 'build': '../commands/build.json', - 'config': '../commands/config.json', - 'doc': '../commands/doc.json', - 'e2e': '../commands/e2e.json', - 'make-this-awesome': '../commands/easter-egg.json', - 'generate': '../commands/generate.json', - 'get': '../commands/deprecated.json', - 'set': '../commands/deprecated.json', - 'help': '../commands/help.json', - 'lint': '../commands/lint.json', - 'new': '../commands/new.json', - 'run': '../commands/run.json', - 'serve': '../commands/serve.json', - 'test': '../commands/test.json', - 'update': '../commands/update.json', - 'version': '../commands/version.json', - 'xi18n': '../commands/xi18n.json', -}; - -export interface CommandMapOptions { - [key: string]: string; -} - -/** - * Create the analytics instance. - * @private - */ -async function _createAnalytics(): Promise { - const config = getGlobalAnalytics(); - const maybeSharedAnalytics = getSharedAnalytics(); - - if (config && maybeSharedAnalytics) { - return new analytics.MultiAnalytics([config, maybeSharedAnalytics]); - } else if (config) { - return config; - } else if (maybeSharedAnalytics) { - return maybeSharedAnalytics; - } else { - return new analytics.NoopAnalytics(); - } -} - -async function loadCommandDescription( - name: string, - path: string, - registry: json.schema.CoreSchemaRegistry, -): Promise { - const schemaPath = resolve(__dirname, path); - const schemaContent = readFileSync(schemaPath, 'utf-8'); - const schema = json.parseJson(schemaContent, JsonParseMode.Loose, { path: schemaPath }); - if (!isJsonObject(schema)) { - throw new Error('Invalid command JSON loaded from ' + JSON.stringify(schemaPath)); - } - - return parseJsonSchemaToCommandDescription(name, schemaPath, registry, schema); -} - -/** - * Run a command. - * @param args Raw unparsed arguments. - * @param logger The logger to use. - * @param workspace Workspace information. - * @param commands The map of supported commands. - * @param options Additional options. - */ -export async function runCommand( - args: string[], - logger: logging.Logger, - workspace: CommandWorkspace, - commands: CommandMapOptions = standardCommands, - options: { analytics?: analytics.Analytics } = {}, -): Promise { - // This registry is exclusively used for flattening schemas, and not for validating. - const registry = new schema.CoreSchemaRegistry([]); - registry.registerUriHandler((uri: string) => { - if (uri.startsWith('ng-cli://')) { - const content = readFileSync(join(__dirname, '..', uri.substr('ng-cli://'.length)), 'utf-8'); - - return Promise.resolve(JSON.parse(content)); - } else { - return null; - } - }); - - let commandName: string | undefined = undefined; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - if (!arg.startsWith('-')) { - commandName = arg; - args.splice(i, 1); - break; - } - } - - let description: CommandDescription | null = null; - - // if no commands were found, use `help`. - if (!commandName) { - if (args.length === 1 && args[0] === '--version') { - commandName = 'version'; - } else { - commandName = 'help'; - } - - if (!(commandName in commands)) { - logger.error(tags.stripIndent` - The "${commandName}" command seems to be disabled. - This is an issue with the CLI itself. If you see this comment, please report it and - provide your repository. - `); - - return 1; - } - } - - if (commandName in commands) { - description = await loadCommandDescription(commandName, commands[commandName], registry); - } else { - const commandNames = Object.keys(commands); - - // Optimize loading for common aliases - if (commandName.length === 1) { - commandNames.sort((a, b) => { - const aMatch = a[0] === commandName; - const bMatch = b[0] === commandName; - if (aMatch && !bMatch) { - return -1; - } else if (!aMatch && bMatch) { - return 1; - } else { - return 0; - } - }); - } - - for (const name of commandNames) { - const aliasDesc = await loadCommandDescription(name, commands[name], registry); - const aliases = aliasDesc.aliases; - - if (aliases && aliases.some(alias => alias === commandName)) { - commandName = name; - description = aliasDesc; - break; - } - } - } - - if (!description) { - const commandsDistance = {} as { [name: string]: number }; - const name = commandName; - const allCommands = Object.keys(commands).sort((a, b) => { - if (!(a in commandsDistance)) { - commandsDistance[a] = strings.levenshtein(a, name); - } - if (!(b in commandsDistance)) { - commandsDistance[b] = strings.levenshtein(b, name); - } - - 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; - } - - try { - const parsedOptions = parser.parseArguments(args, description.options, logger); - Command.setCommandMap(async () => { - const map: Record = {}; - for (const [name, path] of Object.entries(commands)) { - map[name] = await loadCommandDescription(name, path, registry); - } - - return map; - }); - - const analytics = options.analytics || await _createAnalytics(); - const context = { workspace, analytics }; - const command = new description.impl(context, description, logger); - - // Flush on an interval (if the event loop is waiting). - let analyticsFlushPromise = Promise.resolve(); - setInterval(() => { - analyticsFlushPromise = analyticsFlushPromise.then(() => analytics.flush()); - }, 1000); - - const result = await command.validateAndRun(parsedOptions); - - // Flush one last time. - await analyticsFlushPromise.then(() => analytics.flush()); - - return result; - } catch (e) { - if (e instanceof parser.ParseArgumentException) { - logger.fatal('Cannot parse arguments. See below for the reasons.'); - logger.fatal(' ' + e.comments.join('\n ')); - - return 1; - } else { - throw e; - } - } -} diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts deleted file mode 100644 index 0e783fb18318..000000000000 --- a/packages/angular/cli/models/command.ts +++ /dev/null @@ -1,192 +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 { analytics, logging, strings, tags } from '@angular-devkit/core'; -import * as path from 'path'; -import { colors } from '../utilities/color'; -import { getWorkspace } from '../utilities/config'; -import { - Arguments, - CommandContext, - CommandDescription, - CommandDescriptionMap, - CommandScope, - CommandWorkspace, - Option, - SubCommandDescription, -} from './interface'; - -export interface BaseCommandOptions { - help?: boolean | string; -} - -export abstract class Command { - public allowMissingWorkspace = false; - public workspace: CommandWorkspace; - public analytics: analytics.Analytics; - - protected static commandMap: () => Promise; - static setCommandMap(map: () => Promise) { - this.commandMap = map; - } - - constructor( - context: CommandContext, - public readonly description: CommandDescription, - protected readonly logger: logging.Logger, - ) { - this.workspace = context.workspace; - this.analytics = context.analytics || new analytics.NoopAnalytics(); - } - - async initialize(options: T & Arguments): Promise { - return; - } - - async printHelp(options: T & Arguments): Promise { - await this.printHelpUsage(); - await this.printHelpOptions(); - - return 0; - } - - async printJsonHelp(_options: T & Arguments): Promise { - this.logger.info(JSON.stringify(this.description)); - - return 0; - } - - protected async printHelpUsage() { - this.logger.info(this.description.description); - - const name = this.description.name; - const args = this.description.options.filter(x => x.positional !== undefined); - const opts = this.description.options.filter(x => x.positional === undefined); - - const argDisplay = args && args.length > 0 ? ' ' + args.map(a => `<${a.name}>`).join(' ') : ''; - const optionsDisplay = opts && opts.length > 0 ? ` [options]` : ``; - - this.logger.info(`usage: ng ${name}${argDisplay}${optionsDisplay}`); - this.logger.info(''); - } - - protected async printHelpSubcommand(subcommand: SubCommandDescription) { - this.logger.info(subcommand.description); - - await this.printHelpOptions(subcommand.options); - } - - protected async printHelpOptions(options: Option[] = this.description.options) { - const args = options.filter(opt => opt.positional !== undefined); - const opts = options.filter(opt => opt.positional === undefined); - - const formatDescription = (description: string) => - ` ${description.replace(/\n/g, '\n ')}`; - - if (args.length > 0) { - this.logger.info(`arguments:`); - args.forEach(o => { - this.logger.info(` ${colors.cyan(o.name)}`); - if (o.description) { - this.logger.info(formatDescription(o.description)); - } - }); - } - if (options.length > 0) { - if (args.length > 0) { - this.logger.info(''); - } - this.logger.info(`options:`); - opts - .filter(o => !o.hidden) - .sort((a, b) => a.name.localeCompare(b.name)) - .forEach(o => { - const aliases = - o.aliases && o.aliases.length > 0 - ? '(' + o.aliases.map(a => `-${a}`).join(' ') + ')' - : ''; - this.logger.info(` ${colors.cyan('--' + strings.dasherize(o.name))} ${aliases}`); - if (o.description) { - this.logger.info(formatDescription(o.description)); - } - }); - } - } - - async validateScope(scope?: CommandScope): Promise { - switch (scope === undefined ? this.description.scope : scope) { - case CommandScope.OutProject: - if (this.workspace.configFile) { - this.logger.fatal(tags.oneLine` - The ${this.description.name} command requires to be run outside of a project, but a - project definition was found at "${path.join( - this.workspace.root, - this.workspace.configFile, - )}". - `); - throw 1; - } - break; - case CommandScope.InProject: - if (!this.workspace.configFile || getWorkspace('local') === null) { - this.logger.fatal(tags.oneLine` - The ${this.description.name} command requires to be run in an Angular project, but a - project definition could not be found. - `); - throw 1; - } - break; - case CommandScope.Everywhere: - // Can't miss this. - break; - } - } - - async reportAnalytics( - paths: string[], - options: T & Arguments, - dimensions: (boolean | number | string)[] = [], - metrics: (boolean | number | string)[] = [], - ): Promise { - for (const option of this.description.options) { - const ua = option.userAnalytics; - const v = options[option.name]; - - if (v !== undefined && !Array.isArray(v) && ua) { - dimensions[ua] = v; - } - } - - this.analytics.pageview('/command/' + paths.join('/'), { dimensions, metrics }); - } - - abstract async run(options: T & Arguments): Promise; - - async validateAndRun(options: T & Arguments): Promise { - if (!(options.help === true || options.help === 'json' || options.help === 'JSON')) { - await this.validateScope(); - } - await this.initialize(options); - - if (options.help === true) { - return this.printHelp(options); - } else if (options.help === 'json' || options.help === 'JSON') { - return this.printJsonHelp(options); - } else { - const startTime = +new Date(); - await this.reportAnalytics([this.description.name], options); - const result = await this.run(options); - const endTime = +new Date(); - - this.analytics.timing(this.description.name, 'duration', endTime - startTime); - - return result; - } - } -} 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/interface.ts b/packages/angular/cli/models/interface.ts deleted file mode 100644 index 17292c95269b..000000000000 --- a/packages/angular/cli/models/interface.ts +++ /dev/null @@ -1,242 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { analytics, json, logging } from '@angular-devkit/core'; - -/** - * Value type of arguments. - */ -export type Value = number | string | boolean | (number | string | boolean)[]; - -/** - * An object representing parsed arguments from the command line. - */ -export interface Arguments { - [argName: string]: Value | undefined; - - /** - * Extra arguments that were not parsed. Will be omitted if all arguments were parsed. - */ - '--'?: string[]; -} - -/** - * The base interface for Command, understood by the command runner. - */ -export interface CommandInterface { - printHelp(options: T): Promise; - printJsonHelp(options: T): Promise; - validateAndRun(options: T): Promise; -} - -/** - * Command constructor. - */ -export interface CommandConstructor { - new( - context: CommandContext, - description: CommandDescription, - logger: logging.Logger, - ): CommandInterface; -} - -/** - * A CLI workspace information. - */ -export interface CommandWorkspace { - root: string; - configFile?: string; -} - -/** - * A command runner context. - */ -export interface CommandContext { - workspace: CommandWorkspace; - - // This feel is optional for backward compatibility. - analytics?: analytics.Analytics; -} - -/** - * Value types of an Option. - */ -export enum OptionType { - Any = 'any', - Array = 'array', - Boolean = 'boolean', - Number = 'number', - String = 'string', -} - -/** - * An option description. This is exposed when using `ng --help=json`. - */ -export interface Option { - /** - * The name of the option. - */ - name: string; - - /** - * A short description of the option. - */ - description: string; - - /** - * The type of option value. If multiple types exist, this type will be the first one, and the - * types array will contain all types accepted. - */ - type: OptionType; - - /** - * {@see type} - */ - types?: OptionType[]; - - /** - * If this field is set, only values contained in this field are valid. This array can be mixed - * types (strings, numbers, boolean). For example, if this field is "enum: ['hello', true]", - * then "type" will be either string or boolean, types will be at least both, and the values - * accepted will only be either 'hello' or true (not false or any other string). - * This mean that prefixing with `no-` will not work on this field. - */ - enum?: Value[]; - - /** - * If this option maps to a subcommand in the parent command, will contain all the subcommands - * supported. There is a maximum of 1 subcommand Option per command, and the type of this - * option will always be "string" (no other types). The value of this option will map into - * this map and return the extra information. - */ - subcommands?: { - [name: string]: SubCommandDescription; - }; - - /** - * Aliases supported by this option. - */ - aliases: 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; - - /** - * Default value of this option. - */ - default?: string | number | boolean; - - /** - * If this option can be used as an argument, the position of the argument. Otherwise omitted. - */ - positional?: number; - - /** - * Deprecation. If this flag is not false a warning will be shown on the console. Either `true` - * or a string to show the user as a notice. - */ - deprecated?: boolean | string; - - /** - * Smart default object. - */ - $default?: OptionSmartDefault; - - /** - * 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?: number; -} - -/** - * Scope of the command. - */ -export enum CommandScope { - InProject = 'in', - OutProject = 'out', - Everywhere = 'all', - - Default = InProject, -} - -/** - * A description of a command and its options. - */ -export interface SubCommandDescription { - /** - * The name of the subcommand. - */ - name: string; - - /** - * Short description (1-2 lines) of this sub command. - */ - description: string; - - /** - * A long description of the sub command, in Markdown format. - */ - longDescription?: string; - - /** - * Additional notes about usage of this sub command, in Markdown format. - */ - usageNotes?: string; - - /** - * List of all supported options. - */ - options: Option[]; - - /** - * Aliases supported for this sub command. - */ - aliases: string[]; -} - -/** - * A description of a command, its metadata. - */ -export interface CommandDescription extends SubCommandDescription { - /** - * Scope of the command, whether it can be executed in a project, outside of a project or - * anywhere. - */ - scope: CommandScope; - - /** - * Whether this command should be hidden from a list of all commands. - */ - hidden: boolean; - - /** - * The constructor of the command, which should be extending the abstract Command<> class. - */ - impl: CommandConstructor; -} - -export interface OptionSmartDefault { - $source: string; - [key: string]: json.JsonValue; -} - -export interface CommandDescriptionMap { - [key: string]: CommandDescription; -} diff --git a/packages/angular/cli/models/parser.ts b/packages/angular/cli/models/parser.ts deleted file mode 100644 index f37d604aa536..000000000000 --- a/packages/angular/cli/models/parser.ts +++ /dev/null @@ -1,405 +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 { BaseException, logging, strings } from '@angular-devkit/core'; -import { Arguments, Option, OptionType, Value } from './interface'; - - -export class ParseArgumentException extends BaseException { - constructor( - public readonly comments: string[], - public readonly parsed: Arguments, - public readonly ignored: string[], - ) { - super(`One or more errors occurred while parsing arguments:\n ${comments.join('\n ')}`); - } -} - - -function _coerceType(str: string | undefined, type: OptionType, v?: Value): Value | undefined { - switch (type) { - case OptionType.Any: - if (Array.isArray(v)) { - return v.concat(str || ''); - } - - return _coerceType(str, OptionType.Boolean, v) !== undefined - ? _coerceType(str, OptionType.Boolean, v) - : _coerceType(str, OptionType.Number, v) !== undefined - ? _coerceType(str, OptionType.Number, v) - : _coerceType(str, OptionType.String, v); - - case OptionType.String: - return str || ''; - - case OptionType.Boolean: - switch (str) { - case 'false': - return false; - - case undefined: - case '': - case 'true': - return true; - - default: - return undefined; - } - - case OptionType.Number: - if (str === undefined) { - return 0; - } else if (str === '') { - return undefined; - } else if (Number.isFinite(+str)) { - return +str; - } else { - return undefined; - } - - case OptionType.Array: - return Array.isArray(v) - ? v.concat(str || '') - : v === undefined - ? [str || ''] - : [v + '', str || '']; - - default: - return undefined; - } -} - -function _coerce(str: string | undefined, o: Option | null, v?: Value): Value | undefined { - if (!o) { - return _coerceType(str, OptionType.Any, v); - } else { - const types = o.types || [o.type]; - - // Try all the types one by one and pick the first one that returns a value contained in the - // enum. If there's no enum, just return the first one that matches. - for (const type of types) { - const maybeResult = _coerceType(str, type, v); - if (maybeResult !== undefined) { - if (!o.enum || o.enum.includes(maybeResult)) { - return maybeResult; - } - } - } - - return undefined; - } -} - - -function _getOptionFromName(name: string, options: Option[]): Option | undefined { - const camelName = /(-|_)/.test(name) - ? strings.camelize(name) - : name; - - for (const option of options) { - if (option.name === name || option.name === camelName) { - return option; - } - - if (option.aliases.some(x => x === name || x === camelName)) { - return option; - } - } - - return undefined; -} - -function _removeLeadingDashes(key: string): string { - const from = key.startsWith('--') ? 2 : key.startsWith('-') ? 1 : 0; - - return key.substr(from); -} - -function _assignOption( - arg: string, - nextArg: string | undefined, - { options, parsedOptions, leftovers, ignored, errors, warnings }: { - options: Option[], - parsedOptions: Arguments, - positionals: string[], - leftovers: string[], - ignored: string[], - errors: string[], - warnings: string[], - }, -) { - const from = arg.startsWith('--') ? 2 : 1; - let consumedNextArg = false; - let key = arg.substr(from); - let option: Option | null = null; - let value: string | undefined = ''; - const i = arg.indexOf('='); - - // If flag is --no-abc AND there's no equal sign. - if (i == -1) { - if (key.startsWith('no')) { - // Only use this key if the option matching the rest is a boolean. - const from = key.startsWith('no-') ? 3 : 2; - const maybeOption = _getOptionFromName(strings.camelize(key.substr(from)), options); - if (maybeOption && maybeOption.type == 'boolean') { - value = 'false'; - option = maybeOption; - } - } - - if (option === null) { - // Set it to true if it's a boolean and the next argument doesn't match true/false. - const maybeOption = _getOptionFromName(key, options); - if (maybeOption) { - value = nextArg; - let shouldShift = true; - - if (value && value.startsWith('-')) { - // Verify if not having a value results in a correct parse, if so don't shift. - if (_coerce(undefined, maybeOption) !== undefined) { - shouldShift = false; - } - } - - // Only absorb it if it leads to a better value. - if (shouldShift && _coerce(value, maybeOption) !== undefined) { - consumedNextArg = true; - } else { - value = ''; - } - option = maybeOption; - } - } - } else { - key = arg.substring(0, i); - option = _getOptionFromName(_removeLeadingDashes(key), options) || null; - if (option) { - value = arg.substring(i + 1); - } - } - - if (option === null) { - if (nextArg && !nextArg.startsWith('-')) { - leftovers.push(arg, nextArg); - consumedNextArg = true; - } else { - leftovers.push(arg); - } - } else { - const v = _coerce(value, option, parsedOptions[option.name]); - if (v !== undefined) { - if (parsedOptions[option.name] !== v) { - if (parsedOptions[option.name] !== undefined) { - warnings.push( - `Option ${JSON.stringify(option.name)} was already specified with value ` - + `${JSON.stringify(parsedOptions[option.name])}. The new value ${JSON.stringify(v)} ` - + `will override it.`, - ); - } - - parsedOptions[option.name] = v; - - if (option.deprecated !== undefined && option.deprecated !== false) { - warnings.push(`Option ${JSON.stringify(option.name)} is deprecated${ - typeof option.deprecated == 'string' ? ': ' + option.deprecated : '.'}`); - } - } - } else { - let error = `Argument ${key} could not be parsed using value ${JSON.stringify(value)}.`; - if (option.enum) { - error += ` Valid values are: ${option.enum.map(x => JSON.stringify(x)).join(', ')}.`; - } else { - error += `Valid type(s) is: ${(option.types || [option.type]).join(', ')}`; - } - - errors.push(error); - ignored.push(arg); - } - } - - return consumedNextArg; -} - - -/** - * Parse the arguments in a consistent way, but without having any option definition. This tries - * to assess what the user wants in a free form. For example, using `--name=false` will set the - * name properties to a boolean type. - * This should only be used when there's no schema available or if a schema is "true" (anything is - * valid). - * - * @param args Argument list to parse. - * @returns An object that contains a property per flags from the args. - */ -export function parseFreeFormArguments(args: string[]): Arguments { - const parsedOptions: Arguments = {}; - const leftovers = []; - - for (let arg = args.shift(); arg !== undefined; arg = args.shift()) { - if (arg == '--') { - leftovers.push(...args); - break; - } - - if (arg.startsWith('--')) { - const eqSign = arg.indexOf('='); - let name: string; - let value: string | undefined; - if (eqSign !== -1) { - name = arg.substring(2, eqSign); - value = arg.substring(eqSign + 1); - } else { - name = arg.substr(2); - value = args.shift(); - } - - const v = _coerce(value, null, parsedOptions[name]); - if (v !== undefined) { - parsedOptions[name] = v; - } - } else if (arg.startsWith('-')) { - arg.split('').forEach(x => parsedOptions[x] = true); - } else { - leftovers.push(arg); - } - } - - parsedOptions['--'] = leftovers; - - return parsedOptions; -} - - -/** - * Parse the arguments in a consistent way, from a list of standardized options. - * The result object will have a key per option name, with the `_` key reserved for positional - * arguments, and `--` will contain everything that did not match. Any key that don't have an - * option will be pushed back in `--` and removed from the object. If you need to validate that - * there's no additionalProperties, you need to check the `--` key. - * - * @param args The argument array to parse. - * @param options List of supported options. {@see Option}. - * @param logger Logger to use to warn users. - * @returns An object that contains a property per option. - */ -export function parseArguments( - args: string[], - options: Option[] | null, - logger?: logging.Logger, -): Arguments { - if (options === null) { - options = []; - } - - const leftovers: string[] = []; - const positionals: string[] = []; - const parsedOptions: Arguments = {}; - - const ignored: string[] = []; - const errors: string[] = []; - const warnings: string[] = []; - - const state = { options, parsedOptions, positionals, leftovers, ignored, errors, warnings }; - - for (let argIndex = 0; argIndex < args.length; argIndex++) { - const arg = args[argIndex]; - let consumedNextArg = false; - - if (arg == '--') { - // If we find a --, we're done. - leftovers.push(...args.slice(argIndex + 1)); - break; - } - - if (arg.startsWith('--')) { - consumedNextArg = _assignOption(arg, args[argIndex + 1], state); - } else if (arg.startsWith('-')) { - // Argument is of form -abcdef. Starts at 1 because we skip the `-`. - for (let i = 1; i < arg.length; i++) { - const flag = arg[i]; - // If the next character is an '=', treat it as a long flag. - if (arg[i + 1] == '=') { - const f = '-' + flag + arg.slice(i + 1); - consumedNextArg = _assignOption(f, args[argIndex + 1], state); - break; - } - // Treat the last flag as `--a` (as if full flag but just one letter). We do this in - // the loop because it saves us a check to see if the arg is just `-`. - if (i == arg.length - 1) { - const arg = '-' + flag; - consumedNextArg = _assignOption(arg, args[argIndex + 1], state); - } else { - const maybeOption = _getOptionFromName(flag, options); - if (maybeOption) { - const v = _coerce(undefined, maybeOption, parsedOptions[maybeOption.name]); - if (v !== undefined) { - parsedOptions[maybeOption.name] = v; - } - } - } - } - } else { - positionals.push(arg); - } - - if (consumedNextArg) { - argIndex++; - } - } - - // Deal with positionals. - // TODO(hansl): this is by far the most complex piece of code in this file. Try to refactor it - // simpler. - if (positionals.length > 0) { - let pos = 0; - for (let i = 0; i < positionals.length;) { - let found = false; - let incrementPos = false; - let incrementI = true; - - // We do this with a found flag because more than 1 option could have the same positional. - for (const option of options) { - // If any option has this positional and no value, AND fit the type, we need to remove it. - if (option.positional === pos) { - const coercedValue = _coerce(positionals[i], option, parsedOptions[option.name]); - if (parsedOptions[option.name] === undefined && coercedValue !== undefined) { - parsedOptions[option.name] = coercedValue; - found = true; - } else { - incrementI = false; - } - incrementPos = true; - } - } - - if (found) { - positionals.splice(i--, 1); - } - if (incrementPos) { - pos++; - } - if (incrementI) { - i++; - } - } - } - - if (positionals.length > 0 || leftovers.length > 0) { - parsedOptions['--'] = [...positionals, ...leftovers]; - } - - if (warnings.length > 0 && logger) { - warnings.forEach(message => logger.warn(message)); - } - - if (errors.length > 0) { - throw new ParseArgumentException(errors, parsedOptions, ignored); - } - - return parsedOptions; -} diff --git a/packages/angular/cli/models/parser_spec.ts b/packages/angular/cli/models/parser_spec.ts deleted file mode 100644 index 86b9cba71211..000000000000 --- a/packages/angular/cli/models/parser_spec.ts +++ /dev/null @@ -1,241 +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-big-function -import { logging } from '@angular-devkit/core'; -import { Arguments, Option, OptionType } from './interface'; -import { ParseArgumentException, parseArguments } from './parser'; - -describe('parseArguments', () => { - const options: Option[] = [ - { name: 'bool', aliases: [ 'b' ], type: OptionType.Boolean, description: '' }, - { name: 'num', aliases: [ 'n' ], type: OptionType.Number, description: '' }, - { name: 'str', aliases: [ 's' ], type: OptionType.String, description: '' }, - { name: 'strUpper', aliases: [ 'S' ], type: OptionType.String, description: '' }, - { name: 'helloWorld', aliases: [], type: OptionType.String, description: '' }, - { name: 'helloBool', aliases: [], type: OptionType.Boolean, description: '' }, - { name: 'arr', aliases: [ 'a' ], type: OptionType.Array, description: '' }, - { name: 'p1', positional: 0, aliases: [], type: OptionType.String, description: '' }, - { name: 'p2', positional: 1, aliases: [], type: OptionType.String, description: '' }, - { name: 'p3', positional: 2, aliases: [], type: OptionType.Number, description: '' }, - { name: 't1', aliases: [], type: OptionType.Boolean, - types: [OptionType.Boolean, OptionType.String], description: '' }, - { name: 't2', aliases: [], type: OptionType.Boolean, - types: [OptionType.Boolean, OptionType.Number], description: '' }, - { name: 't3', aliases: [], type: OptionType.Number, - types: [OptionType.Number, OptionType.Any], description: '' }, - { name: 'e1', aliases: [], type: OptionType.String, enum: ['hello', 'world'], description: '' }, - { name: 'e2', aliases: [], type: OptionType.String, enum: ['hello', ''], description: '' }, - { name: 'e3', aliases: [], type: OptionType.Boolean, - types: [OptionType.Boolean, OptionType.String], enum: ['json', true, false], - description: '' }, - ]; - - const tests: { [test: string]: Partial | ['!!!', Partial, string[]] } = { - '-S=b': { strUpper: 'b' }, - '--bool': { bool: true }, - '--bool=1': ['!!!', {}, ['--bool=1']], - '--bool ': { bool: true, p1: '' }, - '-- --bool=1': { '--': ['--bool=1'] }, - '--bool=yellow': ['!!!', {}, ['--bool=yellow']], - '--bool=true': { bool: true }, - '--bool=false': { bool: false }, - '--no-bool': { bool: false }, - '--no-bool=true': { '--': ['--no-bool=true'] }, - '--b=true': { bool: true }, - '--b=false': { bool: false }, - '--b true': { bool: true }, - '--b false': { bool: false }, - '--bool --num': { bool: true, num: 0 }, - '--bool --num=true': ['!!!', { bool: true }, ['--num=true']], - '-- --bool --num=true': { '--': ['--bool', '--num=true'] }, - '--bool=true --num': { bool: true, num: 0 }, - '--bool true --num': { bool: true, num: 0 }, - '--bool=false --num': { bool: false, num: 0 }, - '--bool false --num': { bool: false, num: 0 }, - '--str false --num': { str: 'false', num: 0 }, - '--str=false --num': { str: 'false', num: 0 }, - '--str=false --num1': { str: 'false', '--': ['--num1'] }, - '--str=false val1 --num1': { str: 'false', p1: 'val1', '--': ['--num1'] }, - '--str=false val1 val2': { str: 'false', p1: 'val1', p2: 'val2' }, - '--str=false val1 val2 --num1': { str: 'false', p1: 'val1', p2: 'val2', '--': ['--num1'] }, - '--str=false val1 --num1 val2': { str: 'false', p1: 'val1', '--': ['--num1', 'val2'] }, - '--bool --bool=false': { bool: false }, - '--bool --bool=false --bool': { bool: true }, - '--num=1 --num=2 --num=3': { num: 3 }, - '--str=1 --str=2 --str=3': { str: '3' }, - 'val1 --num=1 val2': { num: 1, p1: 'val1', p2: 'val2' }, - '--p1=val1 --num=1 val2': { num: 1, p1: 'val1', p2: 'val2' }, - '--p1=val1 --num=1 --p2=val2 val3': { num: 1, p1: 'val1', p2: 'val2', '--': ['val3'] }, - '--bool val1 --etc --num val2 --v': [ - '!!!', - { bool: true, p1: 'val1', p2: 'val2', '--': ['--etc', '--v'] }, - ['--num' ], - ], - '--bool val1 --etc --num=1 val2 --v': { bool: true, num: 1, p1: 'val1', p2: 'val2', - '--': ['--etc', '--v'] }, - '--arr=a d': { arr: ['a'], p1: 'd' }, - '--arr=a --arr=b --arr c d': { arr: ['a', 'b', 'c'], p1: 'd' }, - '--arr=1 --arr --arr c d': { arr: ['1', '', 'c'], p1: 'd' }, - '--arr=1 --arr --arr c d e': { arr: ['1', '', 'c'], p1: 'd', p2: 'e' }, - '--str=1': { str: '1' }, - '--str=': { str: '' }, - '--str ': { str: '' }, - '--str ': { str: '', p1: '' }, - '--str ': { str: '', p1: '', p2: '', '--': [''] }, - '--hello-world=1': { helloWorld: '1' }, - '--hello-bool': { helloBool: true }, - '--helloBool': { helloBool: true }, - '--no-helloBool': { helloBool: false }, - '--noHelloBool': { helloBool: false }, - '--noBool': { bool: false }, - '-b': { bool: true }, - '-b=true': { bool: true }, - '-sb': { bool: true, str: '' }, - '-s=b': { str: 'b' }, - '-bs': { bool: true, str: '' }, - '--t1=true': { t1: true }, - '--t1': { t1: true }, - '--t1 --num': { t1: true, num: 0 }, - '--no-t1': { t1: false }, - '--t1=yellow': { t1: 'yellow' }, - '--no-t1=true': { '--': ['--no-t1=true'] }, - '--t1=123': { t1: '123' }, - '--t2=true': { t2: true }, - '--t2': { t2: true }, - '--no-t2': { t2: false }, - '--t2=yellow': ['!!!', {}, ['--t2=yellow']], - '--no-t2=true': { '--': ['--no-t2=true'] }, - '--t2=123': { t2: 123 }, - '--t3=a': { t3: 'a' }, - '--t3': { t3: 0 }, - '--t3 true': { t3: true }, - '--e1 hello': { e1: 'hello' }, - '--e1=hello': { e1: 'hello' }, - '--e1 yellow': ['!!!', { p1: 'yellow' }, ['--e1']], - '--e1=yellow': ['!!!', {}, ['--e1=yellow']], - '--e1': ['!!!', {}, ['--e1']], - '--e1 true': ['!!!', { p1: 'true' }, ['--e1']], - '--e1=true': ['!!!', {}, ['--e1=true']], - '--e2 hello': { e2: 'hello' }, - '--e2=hello': { e2: 'hello' }, - '--e2 yellow': { p1: 'yellow', e2: '' }, - '--e2=yellow': ['!!!', {}, ['--e2=yellow']], - '--e2': { e2: '' }, - '--e2 true': { p1: 'true', e2: '' }, - '--e2=true': ['!!!', {}, ['--e2=true']], - '--e3 json': { e3: 'json' }, - '--e3=json': { e3: 'json' }, - '--e3 yellow': { p1: 'yellow', e3: true }, - '--e3=yellow': ['!!!', {}, ['--e3=yellow']], - '--e3': { e3: true }, - '--e3 true': { e3: true }, - '--e3=true': { e3: true }, - 'a b c 1': { p1: 'a', p2: 'b', '--': ['c', '1'] }, - - '-p=1 -c=prod': {'--': ['-p=1', '-c=prod'] }, - '--p --c': {'--': ['--p', '--c'] }, - '--p=123': {'--': ['--p=123'] }, - '--p -c': {'--': ['--p', '-c'] }, - '-p --c': {'--': ['-p', '--c'] }, - '-p --c 123': {'--': ['-p', '--c', '123'] }, - '--c 123 -p': {'--': ['--c', '123', '-p'] }, - }; - - Object.entries(tests).forEach(([str, expected]) => { - it(`works for ${str}`, () => { - try { - const originalArgs = str.split(' '); - const args = originalArgs.slice(); - - const actual = parseArguments(args, options); - - expect(Array.isArray(expected)).toBe(false); - expect(actual).toEqual(expected as Arguments); - expect(args).toEqual(originalArgs); - } catch (e) { - if (!(e instanceof ParseArgumentException)) { - throw e; - } - - // The expected values are an array. - expect(Array.isArray(expected)).toBe(true); - expect(e.parsed).toEqual(expected[1] as Arguments); - expect(e.ignored).toEqual(expected[2] as string[]); - } - }); - }); - - it('handles deprecation', () => { - const options = [ - { name: 'bool', aliases: [], type: OptionType.Boolean, description: '' }, - { name: 'depr', aliases: [], type: OptionType.Boolean, description: '', deprecated: true }, - { name: 'deprM', aliases: [], type: OptionType.Boolean, description: '', deprecated: 'ABCD' }, - ]; - - const logger = new logging.Logger(''); - const messages: string[] = []; - - logger.subscribe(entry => messages.push(entry.message)); - - let result = parseArguments(['--bool'], options, logger); - expect(result).toEqual({ bool: true }); - expect(messages).toEqual([]); - - result = parseArguments(['--depr'], options, logger); - expect(result).toEqual({ depr: true }); - expect(messages.length).toEqual(1); - expect(messages[0]).toMatch(/\bdepr\b/); - messages.shift(); - - result = parseArguments(['--depr', '--bool'], options, logger); - expect(result).toEqual({ depr: true, bool: true }); - expect(messages.length).toEqual(1); - expect(messages[0]).toMatch(/\bdepr\b/); - messages.shift(); - - result = parseArguments(['--depr', '--bool', '--deprM'], options, logger); - expect(result).toEqual({ depr: true, deprM: true, bool: true }); - expect(messages.length).toEqual(2); - expect(messages[0]).toMatch(/\bdepr\b/); - expect(messages[1]).toMatch(/\bdeprM\b/); - expect(messages[1]).toMatch(/\bABCD\b/); - messages.shift(); - }); - - it('handles a flag being added multiple times', () => { - const options = [ - { name: 'bool', aliases: [], type: OptionType.Boolean, description: '' }, - ]; - - const logger = new logging.Logger(''); - const messages: string[] = []; - - logger.subscribe(entry => messages.push(entry.message)); - - let result = parseArguments(['--bool'], options, logger); - expect(result).toEqual({ bool: true }); - expect(messages).toEqual([]); - - result = parseArguments(['--bool', '--bool'], options, logger); - expect(result).toEqual({ bool: true }); - expect(messages).toEqual([]); - - result = parseArguments(['--bool', '--bool=false'], options, logger); - expect(result).toEqual({ bool: false }); - expect(messages.length).toEqual(1); - expect(messages[0]).toMatch(/\bbool\b.*\btrue\b.*\bfalse\b/); - messages.shift(); - - result = parseArguments(['--bool', '--bool=false', '--bool=false'], options, logger); - expect(result).toEqual({ bool: false }); - expect(messages.length).toEqual(1); - expect(messages[0]).toMatch(/\bbool\b.*\btrue\b.*\bfalse\b/); - messages.shift(); - }); -}); diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts deleted file mode 100644 index c6d6fcb6a2a3..000000000000 --- a/packages/angular/cli/models/schematic-command.ts +++ /dev/null @@ -1,574 +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 { - experimental, - json, - logging, - normalize, - schema, - strings, - tags, - virtualFs, -} from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { DryRunEvent, UnsuccessfulWorkflowExecution, workflow } from '@angular-devkit/schematics'; -import { - FileSystemCollection, - FileSystemEngine, - FileSystemSchematic, - NodeWorkflow, - validateOptionsWithSchema, -} from '@angular-devkit/schematics/tools'; -import * as inquirer from 'inquirer'; -import * as systemPath from 'path'; -import { WorkspaceLoader } from '../models/workspace-loader'; -import { colors } from '../utilities/color'; -import { - getProjectByCwd, - getSchematicDefaults, - getWorkspace, - getWorkspaceRaw, -} from '../utilities/config'; -import { parseJsonSchemaToOptions } from '../utilities/json-schema'; -import { getPackageManager } from '../utilities/package-manager'; -import { isTTY } from '../utilities/tty'; -import { isPackageNameSafeForAnalytics } from './analytics'; -import { BaseCommandOptions, Command } from './command'; -import { Arguments, CommandContext, CommandDescription, Option } from './interface'; -import { parseArguments, parseFreeFormArguments } from './parser'; - -export interface BaseSchematicSchema { - debug?: boolean; - dryRun?: boolean; - force?: boolean; - interactive?: boolean; - defaults?: boolean; -} - -export interface RunSchematicOptions extends BaseSchematicSchema { - collectionName: string; - schematicName: string; - additionalOptions?: { [key: string]: {} }; - schematicOptions?: string[]; - showNothingDone?: boolean; -} - -export class UnknownCollectionError extends Error { - constructor(collectionName: string) { - super(`Invalid collection (${collectionName}).`); - } -} - -export abstract class SchematicCommand< - T extends BaseSchematicSchema & BaseCommandOptions -> extends Command { - readonly allowPrivateSchematics: boolean = false; - readonly allowAdditionalArgs: boolean = false; - private _host = new NodeJsSyncHost(); - private _workspace: experimental.workspace.Workspace; - protected _workflow: NodeWorkflow; - - private readonly defaultCollectionName = '@schematics/angular'; - protected collectionName = this.defaultCollectionName; - protected schematicName?: string; - - constructor(context: CommandContext, description: CommandDescription, logger: logging.Logger) { - super(context, description, logger); - } - - public async initialize(options: T & Arguments) { - await this._loadWorkspace(); - this.createWorkflow(options); - - if (this.schematicName) { - // Set the options. - const collection = this.getCollection(this.collectionName); - const schematic = this.getSchematic(collection, this.schematicName, true); - const options = await parseJsonSchemaToOptions( - this._workflow.registry, - schematic.description.schemaJson || {}, - ); - - this.description.options.push(...options.filter(x => !x.hidden)); - - // Remove any user analytics from schematics that are NOT part of our safelist. - for (const o of this.description.options) { - if (o.userAnalytics) { - if (!isPackageNameSafeForAnalytics(this.collectionName)) { - o.userAnalytics = undefined; - } - } - } - } - } - - public async printHelp(options: T & Arguments) { - await super.printHelp(options); - this.logger.info(''); - - const subCommandOption = this.description.options.filter(x => x.subcommands)[0]; - - if (!subCommandOption || !subCommandOption.subcommands) { - return 0; - } - - const schematicNames = Object.keys(subCommandOption.subcommands); - - if (schematicNames.length > 1) { - this.logger.info('Available Schematics:'); - - const namesPerCollection: { [c: string]: string[] } = {}; - schematicNames.forEach(name => { - let [collectionName, schematicName] = name.split(/:/, 2); - if (!schematicName) { - schematicName = collectionName; - collectionName = this.collectionName; - } - - if (!namesPerCollection[collectionName]) { - namesPerCollection[collectionName] = []; - } - - namesPerCollection[collectionName].push(schematicName); - }); - - const defaultCollection = this.getDefaultSchematicCollection(); - Object.keys(namesPerCollection).forEach(collectionName => { - const isDefault = defaultCollection == collectionName; - this.logger.info(` Collection "${collectionName}"${isDefault ? ' (default)' : ''}:`); - - namesPerCollection[collectionName].forEach(schematicName => { - this.logger.info(` ${schematicName}`); - }); - }); - } else if (schematicNames.length == 1) { - this.logger.info('Help for schematic ' + schematicNames[0]); - await this.printHelpSubcommand(subCommandOption.subcommands[schematicNames[0]]); - } - - return 0; - } - - async printHelpUsage() { - const subCommandOption = this.description.options.filter(x => x.subcommands)[0]; - - if (!subCommandOption || !subCommandOption.subcommands) { - return; - } - - const schematicNames = Object.keys(subCommandOption.subcommands); - if (schematicNames.length == 1) { - this.logger.info(this.description.description); - - const opts = this.description.options.filter(x => x.positional === undefined); - const [collectionName, schematicName] = schematicNames[0].split(/:/)[0]; - - // Display if this is not the default collectionName, - // otherwise just show the schematicName. - const displayName = - collectionName == this.getDefaultSchematicCollection() ? schematicName : schematicNames[0]; - - const schematicOptions = subCommandOption.subcommands[schematicNames[0]].options; - const schematicArgs = schematicOptions.filter(x => x.positional !== undefined); - const argDisplay = - schematicArgs.length > 0 - ? ' ' + schematicArgs.map(a => `<${strings.dasherize(a.name)}>`).join(' ') - : ''; - - this.logger.info(tags.oneLine` - usage: ng ${this.description.name} ${displayName}${argDisplay} - ${opts.length > 0 ? `[options]` : ``} - `); - this.logger.info(''); - } else { - await super.printHelpUsage(); - } - } - - protected getEngine(): FileSystemEngine { - return this._workflow.engine; - } - - protected getCollection(collectionName: string): FileSystemCollection { - const engine = this.getEngine(); - const collection = engine.createCollection(collectionName); - - if (collection === null) { - throw new UnknownCollectionError(collectionName); - } - - return collection; - } - - protected getSchematic( - collection: FileSystemCollection, - schematicName: string, - allowPrivate?: boolean, - ): FileSystemSchematic { - return collection.createSchematic(schematicName, allowPrivate); - } - - protected setPathOptions(options: Option[], workingDir: string) { - if (workingDir === '') { - return {}; - } - - return options - .filter(o => o.format === 'path') - .map(o => o.name) - .reduce( - (acc, curr) => { - acc[curr] = workingDir; - - return acc; - }, - {} as { [name: string]: string }, - ); - } - - /* - * Runtime hook to allow specifying customized workflow - */ - protected createWorkflow(options: BaseSchematicSchema): workflow.BaseWorkflow { - if (this._workflow) { - return this._workflow; - } - - const { force, dryRun } = options; - const fsHost = new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(this.workspace.root)); - - const workflow = new NodeWorkflow(fsHost, { - force, - dryRun, - packageManager: getPackageManager(this.workspace.root), - root: normalize(this.workspace.root), - }); - workflow.engineHost.registerContextTransform(context => { - // This is run by ALL schematics, so if someone uses `externalSchematics(...)` which - // is safelisted, it would move to the right analytics (even if their own isn't). - const collectionName: string = context.schematic.collection.description.name; - if (isPackageNameSafeForAnalytics(collectionName)) { - return { - ...context, - analytics: this.analytics, - }; - } else { - return context; - } - }); - - workflow.engineHost.registerOptionsTransform(validateOptionsWithSchema(workflow.registry)); - - if (options.defaults) { - workflow.registry.addPreTransform(schema.transforms.addUndefinedDefaults); - } else { - workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults); - } - - workflow.registry.addSmartDefaultProvider('projectName', () => { - 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; - }); - - if (options.interactive !== false && isTTY()) { - workflow.registry.usePromptProvider((definitions: Array) => { - const questions: inquirer.Questions = definitions.map(definition => { - const question: inquirer.Question = { - name: definition.id, - message: definition.message, - default: definition.default, - }; - - const validator = definition.validator; - if (validator) { - question.validate = input => validator(input); - } - - switch (definition.type) { - case 'confirmation': - question.type = 'confirm'; - break; - case 'list': - question.type = !!definition.multiselect ? 'checkbox' : 'list'; - question.choices = - definition.items && - definition.items.map(item => { - if (typeof item == 'string') { - return item; - } else { - return { - name: item.label, - value: item.value, - }; - } - }); - break; - default: - question.type = definition.type; - break; - } - - return question; - }); - - return inquirer.prompt(questions); - }); - } - - return (this._workflow = workflow); - } - - protected 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 this.defaultCollectionName; - } - - protected async runSchematic(options: RunSchematicOptions) { - const { schematicOptions, debug, dryRun } = options; - let { collectionName, schematicName } = options; - - let nothingDone = true; - let loggingQueue: string[] = []; - let error = false; - - const workflow = this._workflow; - - const workingDir = normalize(systemPath.relative(this.workspace.root, process.cwd())); - - // Get the option object from the schematic schema. - const schematic = this.getSchematic( - this.getCollection(collectionName), - schematicName, - this.allowPrivateSchematics, - ); - // Update the schematic and collection name in case they're not the same as the ones we - // received in our options, e.g. after alias resolution or extension. - collectionName = schematic.collection.description.name; - schematicName = schematic.description.name; - - // TODO: Remove warning check when 'targets' is default - if (collectionName !== this.defaultCollectionName) { - const [ast, configPath] = getWorkspaceRaw('local'); - if (ast) { - const projectsKeyValue = ast.properties.find(p => p.key.value === 'projects'); - if (!projectsKeyValue || projectsKeyValue.value.kind !== 'object') { - return; - } - - const positions: json.Position[] = []; - for (const projectKeyValue of projectsKeyValue.value.properties) { - const projectNode = projectKeyValue.value; - if (projectNode.kind !== 'object') { - continue; - } - const targetsKeyValue = projectNode.properties.find(p => p.key.value === 'targets'); - if (targetsKeyValue) { - positions.push(targetsKeyValue.start); - } - } - - if (positions.length > 0) { - const warning = tags.oneLine` - WARNING: This command may not execute successfully. - The package/collection may not support the 'targets' field within '${configPath}'. - This can be corrected by renaming the following 'targets' fields to 'architect': - `; - - const locations = positions - .map((p, i) => `${i + 1}) Line: ${p.line + 1}; Column: ${p.character + 1}`) - .join('\n'); - - this.logger.warn(warning + '\n' + locations + '\n'); - } - } - } - - // Set the options of format "path". - let o: Option[] | null = null; - let args: Arguments; - - if (!schematic.description.schemaJson) { - args = await this.parseFreeFormArguments(schematicOptions || []); - } else { - o = await parseJsonSchemaToOptions(workflow.registry, schematic.description.schemaJson); - args = await this.parseArguments(schematicOptions || [], o); - } - - // ng-add is special because we don't know all possible options at this point - if (args['--'] && !this.allowAdditionalArgs) { - args['--'].forEach(additional => { - this.logger.fatal(`Unknown option: '${additional.split(/=/)[0]}'`); - }); - - return 1; - } - - const pathOptions = o ? this.setPathOptions(o, workingDir) : {}; - let input = { ...pathOptions, ...args }; - - // Read the default values from the workspace. - const projectName = input.project !== undefined ? '' + input.project : null; - const defaults = getSchematicDefaults(collectionName, schematicName, projectName); - input = { - ...defaults, - ...input, - ...options.additionalOptions, - }; - - 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` - ${colors.white('UPDATE')} ${eventPath} (${event.content.length} bytes) - `); - break; - case 'create': - loggingQueue.push(tags.oneLine` - ${colors.green('CREATE')} ${eventPath} (${event.content.length} bytes) - `); - break; - case 'delete': - loggingQueue.push(`${colors.yellow('DELETE')} ${eventPath}`); - break; - case 'rename': - loggingQueue.push(`${colors.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: input, - debug: debug, - logger: this.logger, - 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: The "dryRun" flag means no changes were made.`); - } - resolve(); - }, - }); - }); - } - - protected async parseFreeFormArguments(schematicOptions: string[]) { - return parseFreeFormArguments(schematicOptions); - } - - protected async parseArguments( - schematicOptions: string[], - options: Option[] | null, - ): Promise { - return parseArguments(schematicOptions, options, this.logger); - } - - private async _loadWorkspace() { - if (this._workspace) { - return; - } - const workspaceLoader = new WorkspaceLoader(this._host); - - try { - this._workspace = await workspaceLoader.loadWorkspace(this.workspace.root); - } catch (err) { - if (!this.allowMissingWorkspace) { - // Ignore missing workspace - throw err; - } - } - } -} diff --git a/packages/angular/cli/models/workspace-loader.ts b/packages/angular/cli/models/workspace-loader.ts deleted file mode 100644 index 854492a2e03b..000000000000 --- a/packages/angular/cli/models/workspace-loader.ts +++ /dev/null @@ -1,54 +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, - normalize, - virtualFs, -} from '@angular-devkit/core'; -import { findUp } from '../utilities/find-up'; - - -export class WorkspaceLoader { - // TODO: add remaining fallbacks. - private _configFileNames = [ - normalize('.angular.json'), - normalize('angular.json'), - ]; - constructor(private _host: virtualFs.Host) { } - - loadWorkspace(projectPath?: string): Promise { - return this._loadWorkspaceFromPath(this._getProjectWorkspaceFilePath(projectPath)); - } - - // TODO: do this with the host instead of fs. - private _getProjectWorkspaceFilePath(projectPath?: string): Path { - // 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 normalize(workspaceFilePath); - } else { - throw new Error(`Local workspace file ('angular.json') could not be found.`); - } - } - - private _loadWorkspaceFromPath(workspacePath: Path) { - const workspaceRoot = dirname(workspacePath); - const workspaceFileName = basename(workspacePath); - const workspace = new experimental.workspace.Workspace(workspaceRoot, this._host); - - return workspace.loadWorkspaceFromHost(workspaceFileName).toPromise(); - } -} diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 37dcc2ce6ddd..5493ad2531ab 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -1,19 +1,16 @@ { "name": "@angular/cli", - "version": "0.0.0", + "version": "0.0.0-PLACEHOLDER", "description": "CLI tool for Angular", "main": "lib/cli/index.js", "bin": { - "ng": "./bin/ng" + "ng": "./bin/ng.js" }, "keywords": [ "angular", "angular-cli", "Angular CLI" ], - "scripts": { - "postinstall": "node ./bin/postinstall/script.js" - }, "repository": { "type": "git", "url": "https://github.com/angular/angular-cli.git" @@ -25,32 +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", + "@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.21", + "@schematics/angular": "workspace:0.0.0-PLACEHOLDER", "@yarnpkg/lockfile": "1.1.0", - "ansi-colors": "4.1.0", - "debug": "^4.1.1", - "ini": "1.3.5", - "inquirer": "6.4.1", - "npm-package-arg": "6.1.0", - "open": "6.4.0", - "pacote": "9.5.1", - "read-package-tree": "5.3.1", - "semver": "6.2.0", - "symbol-observable": "1.2.0", - "universal-analytics": "^0.4.20", - "uuid": "^3.3.2" + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "8.3.2", + "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", "packageGroup": { - "@angular/cli": "0.0.0", - "@angular-devkit/build-angular": "0.0.0", - "@angular-devkit/build-ng-packagr": "0.0.0", - "@angular-devkit/build-webpack": "0.0.0" + "@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 f60fce0f8991..000000000000 --- a/packages/angular/cli/tasks/npm-install.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { logging } from '@angular-devkit/core'; -import { spawn } from 'child_process'; -import { colors } from '../utilities/color'; - -export default async function( - packageName: string, - logger: logging.Logger, - packageManager: string, - projectRoot: string, - save = true, -) { - const installArgs: string[] = []; - switch (packageManager) { - case 'cnpm': - case 'pnpm': - case 'npm': - installArgs.push('install'); - break; - - case 'yarn': - installArgs.push('add'); - break; - - default: - packageManager = 'npm'; - installArgs.push('install'); - break; - } - - logger.info(colors.green(`Installing packages for tooling via ${packageManager}.`)); - - if (packageName) { - installArgs.push(packageName); - } - - if (!save) { - installArgs.push('--no-save'); - } - - installArgs.push('--quiet'); - - await new Promise((resolve, reject) => { - spawn(packageManager, installArgs, { stdio: 'inherit', shell: true }).on( - 'close', - (code: number) => { - if (code === 0) { - logger.info(colors.green(`Installed packages for tooling via ${packageManager}.`)); - resolve(); - } else { - reject('Package install failed, see above.'); - } - }, - ); - }); -} 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/bep.ts b/packages/angular/cli/utilities/bep.ts deleted file mode 100644 index 8acb5ec7cd3b..000000000000 --- a/packages/angular/cli/utilities/bep.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as fs from 'fs'; - - -export interface BuildEventMessage { - id: {}; - [key: string]: {}; -} - -export class BepGenerator { - private constructor() {} - - static createBuildStarted(command: string, time?: number): BuildEventMessage { - return { - id: { started: {} }, - started: { - command, - start_time_millis: time == undefined ? Date.now() : time, - }, - }; - } - - static createBuildFinished(code: number, time?: number): BuildEventMessage { - return { - id: { finished: {} }, - finished: { - finish_time_millis: time == undefined ? Date.now() : time, - exit_code: { code }, - }, - }; - } -} - -export class BepJsonWriter { - private stream = fs.createWriteStream(this.filename); - - constructor(public readonly filename: string) { - - } - - close(): void { - this.stream.close(); - } - - writeEvent(event: BuildEventMessage): void { - const raw = JSON.stringify(event); - - this.stream.write(raw + '\n'); - } - - writeBuildStarted(command: string, time?: number): void { - const event = BepGenerator.createBuildStarted(command, time); - - this.writeEvent(event); - } - - writeBuildFinished(code: number, time?: number): void { - const event = BepGenerator.createBuildFinished(code, time); - - this.writeEvent(event); - } -} diff --git a/packages/angular/cli/utilities/color.ts b/packages/angular/cli/utilities/color.ts deleted file mode 100644 index 59344dfa95f3..000000000000 --- a/packages/angular/cli/utilities/color.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as colors from 'ansi-colors'; -import { WriteStream } from 'tty'; - -// Typings do not contain the function call (added in Node.js v9.9.0) -export const supportsColor = - process.stdout instanceof WriteStream && - ((process.stdout as unknown) as { getColorDepth(): number }).getColorDepth() > 1; - -(colors as { enabled: boolean }).enabled = supportsColor; - -export { colors }; diff --git a/packages/angular/cli/utilities/config.ts b/packages/angular/cli/utilities/config.ts deleted file mode 100644 index 3351b5d7fb4b..000000000000 --- a/packages/angular/cli/utilities/config.ts +++ /dev/null @@ -1,353 +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; -} - -export 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 getConfiguredPackageManager(): string | null { - 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 null; -} - -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 (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 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/json-schema.ts b/packages/angular/cli/utilities/json-schema.ts deleted file mode 100644 index c91a1e213197..000000000000 --- a/packages/angular/cli/utilities/json-schema.ts +++ /dev/null @@ -1,296 +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 { BaseException, json } from '@angular-devkit/core'; -import { ExportStringRef } from '@angular-devkit/schematics/tools'; -import { readFileSync } from 'fs'; -import { dirname, resolve } from 'path'; -import { - CommandConstructor, - CommandDescription, - CommandScope, - Option, - OptionType, - SubCommandDescription, - Value, -} from '../models/interface'; - - -export class CommandJsonPathException extends BaseException { - constructor(public readonly path: string, public readonly name: string) { - super(`File ${path} was not found while constructing the subcommand ${name}.`); - } -} - -function _getEnumFromValue( - value: json.JsonValue, - enumeration: E, - defaultValue: T, -): T { - if (typeof value !== 'string') { - return defaultValue; - } - - if (Object.values(enumeration).indexOf(value) !== -1) { - // TODO: this should be unknown - // tslint:disable-next-line:no-any - return value as any as T; - } - - return defaultValue; -} - -export async function parseJsonSchemaToSubCommandDescription( - name: string, - jsonPath: string, - registry: json.schema.SchemaRegistry, - schema: json.JsonObject, -): Promise { - const options = await parseJsonSchemaToOptions(registry, schema); - - const aliases: string[] = []; - if (json.isJsonArray(schema.$aliases)) { - schema.$aliases.forEach(value => { - if (typeof value == 'string') { - aliases.push(value); - } - }); - } - if (json.isJsonArray(schema.aliases)) { - schema.aliases.forEach(value => { - if (typeof value == 'string') { - aliases.push(value); - } - }); - } - if (typeof schema.alias == 'string') { - aliases.push(schema.alias); - } - - let longDescription = ''; - if (typeof schema.$longDescription == 'string' && schema.$longDescription) { - const ldPath = resolve(dirname(jsonPath), schema.$longDescription); - try { - longDescription = readFileSync(ldPath, 'utf-8'); - } catch (e) { - throw new CommandJsonPathException(ldPath, name); - } - } - let usageNotes = ''; - if (typeof schema.$usageNotes == 'string' && schema.$usageNotes) { - const unPath = resolve(dirname(jsonPath), schema.$usageNotes); - try { - usageNotes = readFileSync(unPath, 'utf-8'); - } catch (e) { - throw new CommandJsonPathException(unPath, name); - } - } - - const description = '' + (schema.description === undefined ? '' : schema.description); - - return { - name, - description, - ...(longDescription ? { longDescription } : {}), - ...(usageNotes ? { usageNotes } : {}), - options, - aliases, - }; -} - -export async function parseJsonSchemaToCommandDescription( - name: string, - jsonPath: string, - registry: json.schema.SchemaRegistry, - schema: json.JsonObject, -): Promise { - const subcommand = - await parseJsonSchemaToSubCommandDescription(name, jsonPath, registry, schema); - - // Before doing any work, let's validate the implementation. - if (typeof schema.$impl != 'string') { - throw new Error(`Command ${name} has an invalid implementation.`); - } - const ref = new ExportStringRef(schema.$impl, dirname(jsonPath)); - const impl = ref.ref; - - if (impl === undefined || typeof impl !== 'function') { - throw new Error(`Command ${name} has an invalid implementation.`); - } - - const scope = _getEnumFromValue(schema.$scope, CommandScope, CommandScope.Default); - const hidden = !!schema.$hidden; - - return { - ...subcommand, - scope, - hidden, - impl, - }; -} - -export async function parseJsonSchemaToOptions( - registry: json.schema.SchemaRegistry, - schema: json.JsonObject, -): 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; - - default: - return false; - } - }).map(x => _getEnumFromValue(x, OptionType, OptionType.String)); - - 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 Value[]; - - 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 type = types[0]; - const $default = current.$default; - const $defaultIndex = (json.isJsonObject($default) && $default['$source'] == 'argv') - ? $default['index'] : undefined; - const positional: number | undefined = typeof $defaultIndex == 'number' - ? $defaultIndex : undefined; - - const required = json.isJsonArray(current.required) - ? current.required.indexOf(name) != -1 : false; - const aliases = 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; - - // 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 xUserAnalytics = current['x-user-analytics']; - const userAnalytics = typeof xUserAnalytics == 'number' ? xUserAnalytics : undefined; - - const option: Option = { - name, - description: '' + (current.description === undefined ? '' : current.description), - ...types.length == 1 ? { type } : { type, types }, - ...defaultValue !== undefined ? { default: defaultValue } : {}, - ...enumValues && enumValues.length > 0 ? { enum: enumValues } : {}, - required, - aliases, - ...format !== undefined ? { format } : {}, - hidden, - ...userAnalytics ? { userAnalytics } : {}, - ...deprecated !== undefined ? { deprecated } : {}, - ...positional !== undefined ? { positional } : {}, - }; - - options.push(option); - } - - const flattenedSchema = await registry.flatten(schema).toPromise(); - json.schema.visitJsonSchema(flattenedSchema, visitor); - - // Sort by positional. - return options.sort((a, b) => { - if (a.positional) { - if (b.positional) { - return a.positional - b.positional; - } else { - return 1; - } - } else if (b.positional) { - return -1; - } else { - return 0; - } - }); -} diff --git a/packages/angular/cli/utilities/json-schema_spec.ts b/packages/angular/cli/utilities/json-schema_spec.ts deleted file mode 100644 index 09822cef7730..000000000000 --- a/packages/angular/cli/utilities/json-schema_spec.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 - * - */ -import { schema } from '@angular-devkit/core'; -import { readFileSync } from 'fs'; -import { join } from 'path'; -import { CommandJsonPathException, parseJsonSchemaToCommandDescription } from './json-schema'; - -describe('parseJsonSchemaToCommandDescription', () => { - let registry: schema.CoreSchemaRegistry; - const baseSchemaJson = { - '$schema': 'http://json-schema.org/schema', - '$id': 'ng-cli://commands/version.json', - 'description': 'Outputs Angular CLI version.', - '$longDescription': 'not a file ref', - - '$aliases': ['v'], - '$scope': 'all', - '$impl': './version-impl#VersionCommand', - - 'type': 'object', - 'allOf': [ - { '$ref': './definitions.json#/definitions/base' }, - ], - }; - - beforeEach(() => { - registry = new schema.CoreSchemaRegistry([]); - registry.registerUriHandler((uri: string) => { - if (uri.startsWith('ng-cli://')) { - const content = readFileSync( - join(__dirname, '..', uri.substr('ng-cli://'.length)), 'utf-8'); - - return Promise.resolve(JSON.parse(content)); - } else { - return null; - } - }); - }); - - it(`should throw on invalid $longDescription path`, async () => { - const name = 'version'; - const schemaPath = join(__dirname, './bad-sample.json'); - const schemaJson = { ...baseSchemaJson, $longDescription: 'not a file ref' }; - try { - await parseJsonSchemaToCommandDescription(name, schemaPath, registry, schemaJson); - } catch (error) { - const refPath = join(__dirname, schemaJson.$longDescription); - expect(error).toEqual(new CommandJsonPathException(refPath, name)); - - return; - } - expect(true).toBe(false, 'function should have thrown'); - }); - - it(`should throw on invalid $usageNotes path`, async () => { - const name = 'version'; - const schemaPath = join(__dirname, './bad-sample.json'); - const schemaJson = { ...baseSchemaJson, $usageNotes: 'not a file ref' }; - try { - await parseJsonSchemaToCommandDescription(name, schemaPath, registry, schemaJson); - } catch (error) { - const refPath = join(__dirname, schemaJson.$usageNotes); - expect(error).toEqual(new CommandJsonPathException(refPath, name)); - - return; - } - expect(true).toBe(false, 'function should have thrown'); - }); -}); diff --git a/packages/angular/cli/utilities/package-manager.ts b/packages/angular/cli/utilities/package-manager.ts deleted file mode 100644 index bf948b1dda5f..000000000000 --- a/packages/angular/cli/utilities/package-manager.ts +++ /dev/null @@ -1,55 +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 { execSync } from 'child_process'; -import { existsSync } from 'fs'; -import { join } from 'path'; -import { getConfiguredPackageManager } from './config'; - -function supports(name: string): boolean { - try { - execSync(`${name} --version`, { stdio: 'ignore' }); - - return true; - } catch { - return false; - } -} - -export function supportsYarn(): boolean { - return supports('yarn'); -} - -export function supportsNpm(): boolean { - return supports('npm'); -} - -export function getPackageManager(root: string): string { - let packageManager = getConfiguredPackageManager(); - if (packageManager) { - return packageManager; - } - - const hasYarn = supportsYarn(); - const hasYarnLock = existsSync(join(root, 'yarn.lock')); - const hasNpm = supportsNpm(); - const hasNpmLock = existsSync(join(root, 'package-lock.json')); - - if (hasYarn && hasYarnLock && !hasNpmLock) { - packageManager = 'yarn'; - } else if (hasNpm && hasNpmLock && !hasYarnLock) { - packageManager = 'npm'; - } else if (hasYarn && !hasNpm) { - packageManager = 'yarn'; - } else if (hasNpm && !hasYarn) { - packageManager = 'npm'; - } - - // 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'; -} diff --git a/packages/angular/cli/utilities/package-metadata.ts b/packages/angular/cli/utilities/package-metadata.ts deleted file mode 100644 index 0775c55792b7..000000000000 --- a/packages/angular/cli/utilities/package-metadata.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { logging } from '@angular-devkit/core'; -import { existsSync, readFileSync } from 'fs'; -import { homedir } from 'os'; -import * as path from 'path'; - -const ini = require('ini'); -const lockfile = require('@yarnpkg/lockfile'); -const pacote = require('pacote'); - -export interface PackageDependencies { - [dependency: string]: string; -} - -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 PackageManifest { - name: string; - version: string; - license?: string; - private?: boolean; - deprecated?: boolean; - - dependencies: PackageDependencies; - devDependencies: PackageDependencies; - peerDependencies: PackageDependencies; - optionalDependencies: PackageDependencies; - - 'ng-add'?: { - - }; - 'ng-update'?: { - migrations: string, - packageGroup: { [name: string]: string }, - }; -} - -export interface PackageMetadata { - name: string; - tags: { [tag: string]: PackageManifest | undefined }; - versions: Map; -} - -let npmrc: { [key: string]: string }; - -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, -): Record { - 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 = [ - path.join(globalPrefix, 'etc', baseFilename), - path.join(homedir(), dotFilename), - ]; - - const projectConfigLocations: string[] = [ - path.join(cwd, dotFilename), - ]; - 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 options: { [key: string]: string } = {}; - for (const location of [...defaultConfigLocations, ...projectConfigLocations]) { - if (existsSync(location)) { - if (showPotentials) { - logger.info(`Trying '${location}'...found.`); - } - - const data = readFileSync(location, 'utf8'); - options = { - ...options, - ...(yarn ? lockfile.parse(data) : ini.parse(data)), - }; - - if (options.cafile) { - const cafile = path.resolve(path.dirname(location), options.cafile); - delete options.cafile; - try { - options.ca = readFileSync(cafile, 'utf8').replace(/\r?\n/, '\\n'); - } catch { } - } - } else if (showPotentials) { - logger.info(`Trying '${location}'...not found.`); - } - } - - // Substitute any environment variable references - for (const key in options) { - if (typeof options[key] === 'string') { - options[key] = options[key].replace(/\$\{([^\}]+)\}/, (_, name) => process.env[name] || ''); - } - } - - return options; -} - -function normalizeManifest(rawManifest: {}): PackageManifest { - // TODO: Fully normalize and sanitize - - return { - dependencies: {}, - devDependencies: {}, - peerDependencies: {}, - optionalDependencies: {}, - // tslint:disable-next-line:no-any - ...rawManifest as any, - }; -} - -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 response = await pacote.packument( - name, - { - 'full-metadata': true, - ...npmrc, - ...(registry ? { registry } : {}), - }, - ); - - // Normalize the response - const metadata: PackageMetadata = { - name: response.name, - tags: {}, - versions: new Map(), - }; - - if (response.versions) { - for (const [version, manifest] of Object.entries(response.versions)) { - metadata.versions.set(version, normalizeManifest(manifest)); - } - } - - if (response['dist-tags']) { - for (const [tag, version] of Object.entries(response['dist-tags'])) { - const manifest = metadata.versions.get(version as string); - 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, verbose, registry } = { - registry: undefined, - usingYarn: false, - verbose: false, - ...options, - }; - - ensureNpmrc(logger, usingYarn, verbose); - - const response = await pacote.manifest( - name, - { - 'full-metadata': true, - ...npmrc, - ...(registry ? { registry } : {}), - }, - ); - - return normalizeManifest(response); -} diff --git a/packages/angular/cli/utilities/package-tree.ts b/packages/angular/cli/utilities/package-tree.ts deleted file mode 100644 index 3005546098eb..000000000000 --- a/packages/angular/cli/utilities/package-tree.ts +++ /dev/null @@ -1,76 +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 interface PackageTreeNodeBase { - name: string; - path: string; - realpath: string; - error?: Error; - id: number; - isLink: boolean; - package: { - name: string; - version: string; - dependencies?: Record; - devDependencies?: Record; - peerDependencies?: Record; - 'ng-update'?: { - migrations?: string; - }; - }; - children: PackageTreeNode[]; -} - -export interface PackageTreeActual extends PackageTreeNodeBase { - isLink: false; - parent?: PackageTreeActual; -} - -export interface PackageTreeLink extends PackageTreeNodeBase { - isLink: true; - parent: null; - target: PackageTreeActual; -} - -export type PackageTreeNode = PackageTreeActual | PackageTreeLink; - -export function readPackageTree(path: string): Promise { - const rpt = require('read-package-tree'); - - return new Promise((resolve, reject) => { - rpt(path, (e: Error | undefined, data: PackageTreeNode) => { - if (e) { - reject(e); - } else { - resolve(data); - } - }); - }); -} - -export function findNodeDependencies(root: PackageTreeNode, node = root) { - const actual = node.isLink ? node.target : node; - - const rawDeps: Record = { - ...actual.package.dependencies, - ...actual.package.devDependencies, - ...actual.package.peerDependencies, - }; - - return Object.entries(rawDeps).reduce( - (deps, [name, version]) => { - const depNode = root.children.find(child => { - return child.name === name && !child.isLink && child.parent === node; - }) as PackageTreeActual | undefined; - - deps[name] = depNode || version; - - return deps; - }, - {} as Record, - ); -} diff --git a/packages/angular/cli/utilities/project.ts b/packages/angular/cli/utilities/project.ts deleted file mode 100644 index 0ec8749d9886..000000000000 --- a/packages/angular/cli/utilities/project.ts +++ /dev/null @@ -1,71 +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 { CommandWorkspace } from '../models/interface'; -import { findUp } from './find-up'; - -export function insideWorkspace(): boolean { - return getWorkspaceDetails() !== null; -} - -export function getWorkspaceDetails(): CommandWorkspace | 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/tty.ts b/packages/angular/cli/utilities/tty.ts deleted file mode 100644 index dd5931e26fb6..000000000000 --- a/packages/angular/cli/utilities/tty.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -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/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 62d5bb02bb1b..000000000000 --- a/packages/angular/pwa/BUILD +++ /dev/null @@ -1,40 +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("@npm_bazel_typescript//:defs.bzl", "ts_library") -load("//tools:ts_json_schema.bzl", "ts_json_schema") - -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 = [ - ":pwa_schema", - "//packages/angular_devkit/core", - "//packages/angular_devkit/schematics", - "@npm//@types/node", - "@npm//rxjs", - ], -) - -ts_json_schema( - name = "pwa_schema", - src = "pwa/schema.json", -) 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 a9ef0a0996b6..4f6fb37b8f99 100644 --- a/packages/angular/pwa/package.json +++ b/packages/angular/pwa/package.json @@ -1,18 +1,27 @@ { "name": "@angular/pwa", - "version": "0.0.0", + "version": "0.0.0-PLACEHOLDER", "description": "PWA schematics for Angular", - "experimental": true, "keywords": [ "blueprints", "code generation", "schematics" ], "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", - "parse5-html-rewriting-stream": "5.1.0" + "@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 + } } } 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.webmanifest b/packages/angular/pwa/pwa/files/root/manifest.webmanifest deleted file mode 100644 index 37cf5fae506f..000000000000 --- a/packages/angular/pwa/pwa/files/root/manifest.webmanifest +++ /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 3389528f8743..f78095b7a564 100644 --- a/packages/angular/pwa/pwa/index.ts +++ b/packages/angular/pwa/pwa/index.ts @@ -1,11 +1,11 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { getSystemPath, join, normalize } from '@angular-devkit/core'; + import { Rule, SchematicsException, @@ -18,22 +18,23 @@ import { template, url, } from '@angular-devkit/schematics'; -import { Readable, Writable } from 'stream'; +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'; -const RewritingStream = require('parse5-html-rewriting-stream'); - function updateIndexFile(path: string): Rule { - return (host: Tree) => { - const buffer = host.read(path); - if (buffer === null) { - throw new SchematicsException(`Could not read index file: ${path}`); - } + return async (host: Tree) => { + const originalContent = host.readText(path); - const rewriter = new RewritingStream(); + const { RewritingStream } = await loadEsmModule( + 'parse5-html-rewriting-stream', + ); + const rewriter = new RewritingStream(); let needsNoScript = true; - rewriter.on('startTag', (startTag: { tagName: string }) => { + rewriter.on('startTag', (startTag) => { if (startTag.tagName === 'noscript') { needsNoScript = false; } @@ -41,7 +42,7 @@ function updateIndexFile(path: string): Rule { rewriter.emitStartTag(startTag); }); - rewriter.on('endTag', (endTag: { tagName: string }) => { + rewriter.on('endTag', (endTag) => { if (endTag.tagName === 'head') { rewriter.emitRaw(' \n'); rewriter.emitRaw(' \n'); @@ -54,44 +55,23 @@ function updateIndexFile(path: string): Rule { rewriter.emitEndTag(endTag); }); - return new Promise(resolve => { - const input = new Readable({ - encoding: 'utf8', - read(): void { - this.push(buffer); - this.push(null); - }, - }); - - const chunks: Array = []; - const output = new Writable({ - write(chunk: string | Buffer, encoding: string, callback: Function): void { - chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, encoding) : chunk); - callback(); - }, - final(callback: (error?: Error) => void): void { - const full = Buffer.concat(chunks); - host.overwrite(path, full.toString()); - callback(); - resolve(); - }, - }); - - input.pipe(rewriter).pipe(output); + 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)); }); }; } -export default function(options: PwaOptions): Rule { - return async host => { +export default function (options: PwaOptions): Rule { + return async (host) => { if (!options.title) { options.title = options.project; } - // Keep Bazel from failing due to deep import - const { getWorkspace, updateWorkspace } = require('@schematics/angular/utility/workspace'); - - const workspace = await getWorkspace(host); + const workspace = await readWorkspace(host); if (!options.project) { throw new SchematicsException('Option "project" is required.'); @@ -114,68 +94,105 @@ export default function(options: PwaOptions): Rule { const buildTargets = []; const testTargets = []; for (const target of project.targets.values()) { - if (target.builder === '@angular-devkit/build-angular:browser') { + 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') { + } else if ( + target.builder === '@angular-devkit/build-angular:karma' || + target.builder === '@angular/build:karma' + ) { testTargets.push(target); } } - // Add manifest to asset configuration - const assetEntry = join(normalize(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] }; - } - } - // Find all index.html files in build targets const indexFiles = new Set(); + let checkForDefaultIndex = false; for (const target of buildTargets) { - if (target.options && typeof target.options.index === 'string') { + if (typeof target.options?.index === 'string') { indexFiles.add(target.options.index); + } else if (target.options?.index === undefined) { + checkForDefaultIndex = true; } if (!target.configurations) { continue; } - for (const configName in target.configurations) { - const configuration = target.configurations[configName]; - if (configuration && typeof configuration.index === 'string') { - indexFiles.add(configuration.index); + + for (const options of Object.values(target.configurations)) { + if (typeof options?.index === 'string') { + indexFiles.add(options.index); + } else if (options?.index === undefined) { + checkForDefaultIndex = true; } } } // Setup sources for the assets files to add to the project - const sourcePath = join(normalize(project.root), 'src'); - const assetsPath = join(sourcePath, 'assets'); - const rootTemplateSource = apply(url('https://github.com/files/root'), [ - template({ ...options }), - move(getSystemPath(sourcePath)), - ]); - const assetsTemplateSource = apply(url('https://github.com/files/assets'), [ - template({ ...options }), - move(getSystemPath(assetsPath)), - ]); + const sourcePath = project.sourceRoot ?? posix.join(project.root, 'src'); + + // 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); + } + } // Setup service worker schematic options - const swOptions = { ...options }; - delete swOptions.title; + 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'; + } - // Chain the rules and return return chain([ - updateWorkspace(workspace), externalSchematic('@schematics/angular', 'service-worker', swOptions), - mergeWith(rootTemplateSource), - mergeWith(assetsTemplateSource), - ...[...indexFiles].map(path => updateIndexFile(path)), + mergeWith( + apply(url('https://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 a6cd6afb90b0..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, @@ -43,105 +39,174 @@ describe('PWA Schematic', () => { }; beforeEach(async () => { - appTree = schematicRunner.runExternalSchematic('@schematics/angular', 'workspace', workspaceOptions); - appTree = await schematicRunner.runExternalSchematicAsync( + appTree = await schematicRunner.runExternalSchematic( + '@schematics/angular', + 'workspace', + workspaceOptions, + ); + appTree = await schematicRunner.runExternalSchematic( '@schematics/angular', 'application', appOptions, appTree, - ).toPromise(); + ); }); - it('should run the service worker schematic', (done) => { - schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { - 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); - done(); - }, done.fail); + 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 create icon files', (done) => { - const dimensions = [72, 96, 128, 144, 152, 192, 384, 512]; - const iconPath = '/projects/bar/src/assets/icons/icon-'; - schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { - dimensions.forEach(d => { - const path = `${iconPath}${d}x${d}.png`; - expect(tree.exists(path)).toEqual(true); - }); - done(); - }, done.fail); + 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 create a manifest file', (done) => { - schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { - expect(tree.exists('/projects/bar/src/manifest.webmanifest')).toEqual(true); - done(); - }, done.fail); + 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).toBe('projects/bar/ngsw-config.json'); + }); + + 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', (done) => { - schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { - const manifestText = tree.readContent('/projects/bar/src/manifest.webmanifest'); - const manifest = JSON.parse(manifestText); + 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); - done(); - }, done.fail); + 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', (done) => { - const options = {...defaultOptions, title: undefined}; - schematicRunner.runSchematicAsync('ng-add', options, appTree).toPromise().then(tree => { - const manifestText = tree.readContent('/projects/bar/src/manifest.webmanifest'); - const manifest = JSON.parse(manifestText); + 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); - expect(manifest.name).toEqual(defaultOptions.project); - expect(manifest.short_name).toEqual(defaultOptions.project); - done(); - }, done.fail); + 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', (done) => { - schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { - const content = tree.readContent('projects/bar/src/index.html'); + 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(/